Merge pull request #418 from JasonLovesDoggo/master

merging my fork into the main develop branch
pull/441/head
Jason 3 years ago committed by GitHub
commit 2db7c1254e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -5,29 +5,34 @@ REDDIT_CLIENT_SECRET=""
REDDIT_USERNAME=""
REDDIT_PASSWORD=""
# Valid options are "yes" and "no"
# Whether or not 2FA is enabled on the reddit account. Default: "no"
REDDIT_2FA=""
# If no, it will ask you a thread link to extract the thread, if yes it will randomize it. Default: "no"
RANDOM_THREAD=""
# Valid options are "light" and "dark". Default: "light"
THEME=""
# Enter a subreddit, e.g. "AskReddit". If this isn't defined, you'll be asked when
# the programme runs. Simply press Enter if you wish to use the default "AskReddit" subreddit when prompted.
SUBREDDIT=""
# Filters the comments by range of lenght (min and max characters)
# Min has to be less or equal to max
# DO NOT INSERT ANY SPACES BETWEEN THE COMMA AND THE VALUES
COMMENT_LENGTH_RANGE = "min,max"
# Range is 0 -> 1. Default: "0.9"
OPACITY="0.9"
# The absolute path of the folder where you want to save the final video
# If empty or wrong, the path will be 'assets/'
FINAL_VIDEO_PATH=""
# Valid options are "yes" and "no" for the variable below
REDDIT_2FA=""
SUBREDDIT="AskReddit"
# True or False
ALLOW_NSFW="False"
# Used if you want to use a specific post. example of one is urdtfx
POST_ID=""
#set to either LIGHT or DARK
THEME="LIGHT"
# used if you want to run multiple times. set to an int e.g. 4 or 29 and leave blank for once
TIMES_TO_RUN=""
MAX_COMMENT_LENGTH="500"
# Range is 0 -> 1 recommended around 0.8-0.9
OPACITY="1"
# see TTSwrapper.py for all valid options
VOICE="en_us_001" # e.g. en_us_002
# IN-PROGRESS - not yet implemented
STORYMODE="False"

@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"

@ -0,0 +1,72 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
- cron: '16 14 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

7
.gitignore vendored

@ -163,3 +163,10 @@ assets/
out
.DS_Store
.setup-done-before
assets/
results/*
.env
reddit-bot-351418-5560ebc49cac.json
/.idea
*.pyc
/video_creation/data/videos.json

@ -1,49 +1,53 @@
# Reddit Video Maker Bot 🎥
https://user-images.githubusercontent.com/6053155/170525726-2db23ae0-97b8-4bd1-8c95-00da60ce099f.mp4
All done WITHOUT video editing or asset compiling. Just pure ✨programming magic✨.
Created by Lewis Menelaws & [TMRRW](https://tmrrwinc.ca)
[
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/6053155/170528535-e274dc0b-7972-4b27-af22-637f8c370133.png">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/6053155/170528582-cb6671e7-5a2f-4bd4-a048-0e6cfa54f0f7.png">
<img src="https://user-images.githubusercontent.com/6053155/170528582-cb6671e7-5a2f-4bd4-a048-0e6cfa54f0f7.png" width="350">
](https://tmrrwinc.ca)
<img src="https://user-images.githubusercontent.com/6053155/170528582-cb6671e7-5a2f-4bd4-a048-0e6cfa54f0f7.png" width="350" alt="img">
## 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...
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.
- **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 during installation)
- Playwright (this should install automatically in installation)
- Sox
## Installation 👩‍💻
1. Clone this repository
2. Run `pip3 install -r requirements.txt`
3. Run `python3 -m playwright install` and `python3 -m playwright install-deps`.
2. 2a **Automatic Install**: Run `python3 main.py` and type 'yes' to activate the setup assistant.
2b **Manual Install**: 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` file, along with whether your account uses two-factor authentication.
3. install [SoX](https://sourceforge.net/projects/sox/files/sox/)
4.
4a **Automatic Install**: Run `python3 main.py` and type 'yes' to activate the setup assistant.
5. Run `pip3 install -r requirements.txt`
6. Run `playwright install` and `playwright install-deps`.
4b **Manual Install**: 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` file, along with whether your account uses two-factor authentication.
7. Run `python3 main.py` (unless you chose automatic install, then the installer will automatically run main.py)
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` file, along with whether your account uses two-factor authentication.
8. Enjoy 😎
5. Run `python3 main.py` (unless you chose automatic install, then the installer will automatically run main.py)
7. Enjoy 😎
## Video
https://user-images.githubusercontent.com/66544866/173453972-6526e4e6-c6ef-41c5-ab40-5d275e724e7c.mp4
If you want to see more detailed guide, please refer to the official [documentation](https://luka-hietala.gitbook.io/documentation-for-the-reddit-bot/).
\*The Documentation is still being developed and worked on, please be patient as we change / add new knowledge!
## Contributing & Ways to improve 📈
@ -51,13 +55,14 @@ In its current state, this bot does exactly what it needs to do. However, lots o
I have tried to simplify the code so anyone can read it and start contributing at any skill level. Don't be shy :) contribute!
To-Do:
- [ ] Creating better documentation and adding a command line interface.
- [x] Allowing users to choose a reddit thread instead of being randomized.
- [x] Allowing users to choose a background that is picked instead of the Minecraft one.
- [x] Allowing users to choose between any subreddit.
- [ ] Allowing users to change voice.
- [ ] Creating better documentation and adding a command line interface.
- [x] Allowing users to change voice.
- [x] Checks if a video has already been created
- [x] Light and Dark modes
- [x] Nsfw post filter
Please read our [contributing guidelines](CONTRIBUTING.md) for more detailed information.
@ -65,6 +70,8 @@ Please read our [contributing guidelines](CONTRIBUTING.md) for more detailed inf
Elebumm (Lewis#6305) - https://github.com/elebumm (Founder)
Jason (JasonLovesDoggo#1904) - https://github.com/JasonLovesDoggo
CallumIO - https://github.com/CallumIO
HarryDaDev (hrvyy#9677) - https://github.com/ImmaHarry

Binary file not shown.

Binary file not shown.

@ -1,115 +1,72 @@
#!/usr/bin/env python3
# Main
from utils.console import print_markdown
from rich.console import Console
import time
from subprocess import Popen
from dotenv import load_dotenv
from os import getenv, name
from reddit.subreddit import get_subreddit_threads
from utils.cleanup import cleanup
from utils.console import print_markdown, print_step
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
from dotenv import load_dotenv
import time, os
console = Console()
configured = True
REQUIRED_VALUES = [
"REDDIT_CLIENT_ID",
"REDDIT_CLIENT_SECRET",
"REDDIT_USERNAME",
"REDDIT_PASSWORD",
"OPACITY",
]
# Banner may look bad or wrong in IDE/Text Editor, but looks perfect in CMD, BASH or ZSH
banner = '''
from video_creation.screenshot_downloader import download_screenshots_of_reddit_posts
from video_creation.voices import save_text_to_mp3
'''
banner = """
"""
print(banner)
time.sleep(0.5)
load_dotenv()
# Modified by JasonLovesDoggo
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."
)
"""
Load .env file if exists. If it doesnt exist, print a warning and launch the setup wizard.
If there is a .env file, check if the required variables are set. If not, print a warning and launch the setup wizard.
"""
time.sleep(1)
client_id = os.getenv("REDDIT_CLIENT_ID")
client_secret = os.getenv("REDDIT_CLIENT_SECRET")
username = os.getenv("REDDIT_USERNAME")
password = os.getenv("REDDIT_PASSWORD")
reddit2fa = os.getenv("REDDIT_2FA")
client_id = getenv("REDDIT_CLIENT_ID")
client_secret = getenv("REDDIT_CLIENT_SECRET")
username = getenv("REDDIT_USERNAME")
password = getenv("REDDIT_PASSWORD")
reddit2fa = getenv("REDDIT_2FA")
load_dotenv()
console.log("[bold green]Checking environment variables...")
time.sleep(.5)
def main():
cleanup()
if not os.path.exists(".env"):
configured = False
console.log("[red] Your .env file is invalid, or was never created. Standby.")
def get_obj():
reddit_obj = get_subreddit_threads()
return reddit_obj
for val in REQUIRED_VALUES:
if val not in os.environ or not os.getenv(val):
console.log(f'[bold red]Missing Variable: "{val}"')
configured = False
console.log(
"[red]Looks like you need to set your Reddit credentials in the .env file. Please follow the instructions in the README.md file to set them up."
)
time.sleep(0.5)
console.log(
"[red]We can also launch the easy setup wizard. type yes to launch it, or no to quit the program."
)
setup_ask = input("Launch setup wizard? > ")
if setup_ask == "yes":
console.log("[bold green]Here goes nothing! Launching setup wizard...")
time.sleep(0.5)
os.system("python3 setup.py")
reddit_object = get_obj()
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, length)
elif setup_ask == "no":
console.print("[red]Exiting...")
time.sleep(0.5)
exit()
else:
console.print("[red]I don't understand that. Exiting...")
time.sleep(0.5)
exit()
def run_many(times):
for x in range(times):
x = x + 1
print_step(
f'on the {x}{("st" if x == 1 else ("nd" if x == 2 else ("rd" if x == 3 else "th")))} iteration of {times}'
) # correct 1st 2nd 3rd 4th 5th....
main()
Popen("cls" if name == "nt" else "clear", shell=True).wait()
if __name__ == "__main__":
try:
float(os.getenv("OPACITY"))
except ValueError:
console.log(
"[red]Please ensure that OPACITY is set between 0 and 1 in your .env file"
)
configured = False
if getenv("TIMES_TO_RUN") and isinstance(int(getenv("TIMES_TO_RUN")), int):
run_many(int(getenv("TIMES_TO_RUN")))
else:
main()
except KeyboardInterrupt:
print_markdown("## Clearing temp files")
cleanup()
exit()
console.log("[bold green]Enviroment Variables are set! Continuing...")
if configured:
reddit_object = get_subreddit_threads()
length, number_of_comments = save_text_to_mp3(reddit_object)
download_screenshots_of_reddit_posts(
reddit_object, number_of_comments, os.getenv("THEME", "light")
)
while True:
vidpath = download_background(length)
noerror = chop_background_video(length, vidpath)
if noerror is True:
break
final_video = make_final_video(number_of_comments)

@ -1,96 +1,97 @@
from numpy import Infinity
from rich.console import Console
from utils.console import print_step, print_substep, print_markdown
from dotenv import load_dotenv
import os, random, praw, re
from os import getenv, environ
import praw
from utils.console import print_step, print_substep
from utils.subreddit import get_subreddit_undone
from utils.videos import check_done
from praw.models import MoreComments
TEXT_WHITELIST = set("abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890")
def textify(text):
return "".join(filter(TEXT_WHITELIST.__contains__, text))
console = Console()
def get_subreddit_threads():
global submission
"""
Returns a list of threads from the provided subreddit.
Returns a list of threads from the AskReddit subreddit.
"""
global submission
print_substep("Logging into Reddit.")
load_dotenv()
if os.getenv("REDDIT_2FA", default="no").casefold() == "yes":
content = {}
if str(getenv("REDDIT_2FA")).casefold() == "yes":
print(
"\nEnter your two-factor authentication code from your authenticator app.\n"
)
code = input("> ")
print()
pw = os.getenv("REDDIT_PASSWORD")
pw = getenv("REDDIT_PASSWORD")
passkey = f"{pw}:{code}"
else:
passkey = os.getenv("REDDIT_PASSWORD")
content = {}
passkey = getenv("REDDIT_PASSWORD")
reddit = praw.Reddit(
client_id=os.getenv("REDDIT_CLIENT_ID"),
client_secret=os.getenv("REDDIT_CLIENT_SECRET"),
user_agent="Accessing subreddit threads",
username=os.getenv("REDDIT_USERNAME"),
password=passkey,
client_id=getenv("REDDIT_CLIENT_ID"),
client_secret=getenv("REDDIT_CLIENT_SECRET"),
user_agent="Accessing Reddit threads",
username=getenv("REDDIT_USERNAME"),
passkey=passkey,
check_for_async=False,
)
# If the user specifies that he doesnt want a random thread, or if he doesn't insert the "RANDOM_THREAD" variable at all, ask the thread link
if not os.getenv("RANDOM_THREAD") or os.getenv("RANDOM_THREAD") == "no":
print_substep("Insert the full thread link:", style="bold green")
thread_link = input()
print_step("Getting the inserted thread...")
submission = reddit.submission(url=thread_link)
else:
# Otherwise, picks a random thread from the inserted subreddit
if os.getenv("SUBREDDIT"):
subreddit = reddit.subreddit(re.sub(r"r\/", "", os.getenv("SUBREDDIT")))
else:
# Prompt the user to enter a subreddit
try:
"""
Ask user for subreddit input
"""
print_step("Getting subreddit threads...")
if not getenv(
"SUBREDDIT"
): # note to self. you can have multiple subreddits via reddit.subreddit("redditdev+learnpython")
subreddit = reddit.subreddit(
re.sub(
r"r\/",
"",
input("What subreddit would you like to pull from? "),
)
input("What subreddit would you like to pull from? ")
) # if the env isnt set, ask user
else:
print_substep(
f"Using subreddit: r/{getenv('SUBREDDIT')} from environment variable config"
)
except ValueError:
subreddit = reddit.subreddit("askreddit")
print_substep("Subreddit not defined. Using AskReddit.")
subreddit = reddit.subreddit(
getenv("SUBREDDIT")
) # Allows you to specify in .env. Done for automation purposes.
if getenv("POST_ID"):
submission = reddit.submission(id=getenv("POST_ID"))
else:
threads = subreddit.hot(limit=25)
submission = list(threads)[random.randrange(0, 25)]
print_substep(f"Video will be: {submission.title}")
print("Getting video comments...")
submission = get_subreddit_undone(threads, subreddit)
submission = check_done(submission) # double checking
if submission is None:
return get_subreddit_threads() # submission already done. rerun
upvotes = submission.score
ratio = submission.upvote_ratio * 100
num_comments = submission.num_comments
console.log(f"[bold green] Video will be: {submission.title} :thumbsup:")
console.log(f"[bold blue] Thread has " + str(upvotes) + " upvotes")
console.log(f"[bold blue] Thread has a upvote ratio of " + str(ratio) + "%")
console.log(f"[bold blue] Thread has " + str(num_comments) + " comments")
console.log("Getting video comments...")
print_substep(f"Video will be: {submission.title} :thumbsup:", style="bold green")
print_substep(f"Thread has " + str(upvotes) + " upvotes", style="bold blue")
print_substep(
f"Thread has a upvote ratio of " + str(ratio) + "%", style="bold blue"
)
print_substep(f"Thread has " + str(num_comments) + " comments", style="bold blue")
environ["VIDEO_TITLE"] = str(
textify(submission.title)
) # todo use global instend of env vars
environ["VIDEO_ID"] = str(textify(submission.id))
try:
content["thread_url"] = submission.url
content["thread_url"] = f"https://reddit.com{submission.permalink}"
content["thread_title"] = submission.title
content["thread_post"] = submission.selftext
# content["thread_content"] = submission.content
content["comments"] = []
for top_level_comment in submission.comments:
COMMENT_LENGTH_RANGE = [0, Infinity]
# Ensure all values are numeric before attempting to cast
if os.getenv("COMMENT_LENGTH_RANGE") and (False not in list(map(lambda arg: arg.isnumeric(), os.getenv("COMMENT_LENGTH_RANGE").split(",")))):
try:
COMMENT_LENGTH_RANGE = [int(i) for i in os.getenv("COMMENT_LENGTH_RANGE").split(",")]
except TypeError:
pass
if COMMENT_LENGTH_RANGE[0] <= len(top_level_comment.body) <= COMMENT_LENGTH_RANGE[1]:
if isinstance(top_level_comment, MoreComments):
continue
if top_level_comment.body in ["[removed]", "[deleted]"]:
continue # # see https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/78
if not top_level_comment.stickied:
if len(top_level_comment.body) <= int(environ["MAX_COMMENT_LENGTH"]):
content["comments"].append(
{
"comment_body": top_level_comment.body,
@ -98,8 +99,5 @@ def get_subreddit_threads():
"comment_id": top_level_comment.id,
}
)
except AttributeError:
pass
print_substep("Received AskReddit threads successfully.", style="bold green")
print_substep("Received subreddit threads Successfully.", style="bold green")
return content

@ -4,5 +4,7 @@ mutagen==1.45.1
playwright==1.22.0
praw==7.6.0
python-dotenv==0.20.0
pytube==12.1.0
requests==2.28.0
rich==12.4.4
yt_dlp==2022.5.18
sox==1.4.1

@ -0,0 +1,24 @@
import os
from os.path import exists
def cleanup() -> int:
if exists("./assets/temp"):
count = 0
files = [
f for f in os.listdir(".") if f.endswith(".mp4") and "temp" in f.lower()
]
count += len(files)
for f in files:
os.remove(f)
try:
for file in os.listdir("./assets/temp/mp4"):
count += 1
os.remove("./assets/temp/mp4/" + file)
except FileNotFoundError:
pass
for file in os.listdir("./assets/temp/mp3"):
count += 1
os.remove("./assets/temp/mp3/" + file)
return count
return 0

@ -0,0 +1,30 @@
from typing import List
import json
from os import getenv
from utils.console import print_substep
def get_subreddit_undone(submissions: List, subreddit):
"""
recursively checks if the top submission in the list was already done.
"""
with open("./video_creation/data/videos.json", "r") as done_vids_raw:
done_videos = json.load(done_vids_raw)
for submission in submissions:
if already_done(done_videos, submission):
continue
if submission.over_18:
if getenv("ALLOW_NSFW").casefold() == "false":
print_substep("NSFW Post Detected. Skipping...")
continue
return submission
return get_subreddit_undone(
subreddit.top(time_filter="hour"), subreddit
) # all of the videos in hot have already been done
def already_done(done_videos: list, submission):
for video in done_videos:
if video["id"] == str(submission):
return True
return False

@ -0,0 +1,21 @@
import json
from os import getenv
from utils.console import print_step
def check_done(redditobj): # don't set this to be run anyplace that isn't subreddit.py bc of inspect stack
"""params:
reddit_object: The Reddit Object you received in askreddit.py"""
with open("./video_creation/data/videos.json", "r") as done_vids_raw:
done_videos = json.load(done_vids_raw)
for video in done_videos:
if video["id"] == str(redditobj):
if getenv("POST_ID"):
print_step(
"You already have done this video but since it was declared specifically in the .env file the program will continue"
)
return redditobj
print_step("Getting new post as the current one has already been done")
return None
return redditobj

@ -0,0 +1,22 @@
import re
def sanitize_text(text):
"""
Sanitizes the text for tts.
What gets removed:
- following characters`^_~@!&;#:-%“”‘"%*/{}[]()\|<>?=+`
- any http or https links
"""
# remove any urls from the text
regex_urls = r"((http|https)\:\/\/)?[a-zA-Z0-9\.\/\?\:@\-_=#]+\.([a-zA-Z]){2,6}([a-zA-Z0-9\.\&\/\?\:@\-_=#])*"
result = re.sub(regex_urls, " ", text)
# note: not removing apostrophes
regex_expr = r"\s['|]|['|]\s|[\^_~@!&;#:\-%“”‘\"%\*/{}\[\]\(\)\\|<>=+]"
result = re.sub(regex_expr, " ", result)
# remove extra whitespace
return " ".join(result.split())

@ -0,0 +1,144 @@
import base64
import os
import random
import re
import requests
import sox
from moviepy.audio.AudioClip import concatenate_audioclips, CompositeAudioClip
from moviepy.audio.io.AudioFileClip import AudioFileClip
from requests.adapters import HTTPAdapter, Retry
# from profanity_filter import ProfanityFilter
# pf = ProfanityFilter()
# Code by @JasonLovesDoggo
# https://twitter.com/scanlime/status/1512598559769702406
nonhuman = [ # DISNEY VOICES
"en_us_ghostface", # Ghost Face
"en_us_chewbacca", # Chewbacca
"en_us_c3po", # C3PO
"en_us_stitch", # Stitch
"en_us_stormtrooper", # Stormtrooper
"en_us_rocket", # Rocket
# ENGLISH VOICES
]
human = [
"en_au_001", # English AU - Female
"en_au_002", # English AU - Male
"en_uk_001", # English UK - Male 1
"en_uk_003", # English UK - Male 2
"en_us_001", # English US - Female (Int. 1)
"en_us_002", # English US - Female (Int. 2)
"en_us_006", # English US - Male 1
"en_us_007", # English US - Male 2
"en_us_009", # English US - Male 3
"en_us_010",
]
voices = nonhuman + human
noneng = [
"fr_001", # French - Male 1
"fr_002", # French - Male 2
"de_001", # German - Female
"de_002", # German - Male
"es_002", # Spanish - Male
# AMERICA VOICES
"es_mx_002", # Spanish MX - Male
"br_001", # Portuguese BR - Female 1
"br_003", # Portuguese BR - Female 2
"br_004", # Portuguese BR - Female 3
"br_005", # Portuguese BR - Male
# ASIA VOICES
"id_001", # Indonesian - Female
"jp_001", # Japanese - Female 1
"jp_003", # Japanese - Female 2
"jp_005", # Japanese - Female 3
"jp_006", # Japanese - Male
"kr_002", # Korean - Male 1
"kr_003", # Korean - Female
"kr_004", # Korean - Male 2
]
# good_voices = {'good': ['en_us_002', 'en_us_006'],
# 'ok': ['en_au_002', 'en_uk_001']} # less en_us_stormtrooper more less en_us_rocket en_us_ghostface
class TTTTSWrapper: # TikTok Text-to-Speech Wrapper
def __init__(self):
self.URI_BASE = "https://api16-normal-useast5.us.tiktokv.com/media/api/text/speech/invoke/?text_speaker="
def tts(
self,
req_text: str = "TikTok Text To Speech",
filename: str = "title.mp3",
random_speaker: bool = False,
censer=False,
):
req_text = req_text.replace("+", "plus").replace(" ", "+").replace("&", "and")
if censer:
# req_text = pf.censor(req_text)
pass
voice = (
self.randomvoice()
if random_speaker
else (os.getenv("VOICE") or random.choice(human))
)
chunks = [
m.group().strip() for m in re.finditer(r" *((.{0,299})(\.|.$))", req_text)
]
audio_clips = []
cbn = sox.Combiner()
#cbn.set_input_format(file_type=["mp3" for _ in chunks])
chunkId = 0
for chunk in chunks:
try:
r = requests.post(
f"{self.URI_BASE}{voice}&req_text={chunk}&speaker_map_type=0"
)
except requests.exceptions.SSLError:
# https://stackoverflow.com/a/47475019/18516611
session = requests.Session()
retry = Retry(connect=3, backoff_factor=0.5)
adapter = HTTPAdapter(max_retries=retry)
session.mount("http://", adapter)
session.mount("https://", adapter)
r = session.post(
f"{self.URI_BASE}{voice}&req_text={chunk}&speaker_map_type=0"
)
vstr = [r.json()["data"]["v_str"]][0]
b64d = base64.b64decode(vstr)
with open(filename.replace(".mp3", f"-{chunkId}.mp3"), "wb") as out:
out.write(b64d)
audio_clips.append(filename.replace(".mp3", f"-{chunkId}.mp3"))
chunkId = chunkId + 1
try:
if len(audio_clips) > 1:
cbn.convert(samplerate=44100, n_channels=2)
cbn.build(audio_clips, filename, "concatenate")
else:
os.rename(audio_clips[0], filename)
except sox.core.SoxError: # https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/67#issuecomment-1150466339
for clip in audio_clips:
i = audio_clips.index(clip) # get the index of the clip
audio_clips = (
audio_clips[:i] + [AudioFileClip(clip)] + audio_clips[i + 1:]
) # replace the clip with an AudioFileClip
audio_concat = concatenate_audioclips(audio_clips)
audio_composite = CompositeAudioClip([audio_concat])
audio_composite.write_audiofile(filename, 44100, 2, 2000, None)
@staticmethod
def randomvoice():
ok_or_good = random.randrange(1, 10)
if ok_or_good == 1: # 1/10 chance of ok voice
return random.choice(voices)
return random.choice(human) # 9/10 chance of good voice

@ -1,9 +1,8 @@
#!/usr/bin/env python3
from random import randrange
from yt_dlp import YoutubeDL
import random
from os import listdir, environ
from pathlib import Path
from random import randrange
from pytube import YouTube
from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
from moviepy.editor import VideoFileClip
from utils.console import print_step, print_substep
@ -12,63 +11,53 @@ import datetime
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(video_length):
"""Downloads the background video from youtube.
Shoutout to: bbswitzer (https://www.youtube.com/watch?v=n_Dv4JMiwK8)
"""
print_substep("\nPut the URL of the video you want in the background.\nThe default video is a Minecraft parkour video.\n"
"Leave the input field blank to use the default.")
print_substep(f"Make sure the video is longer than {str(datetime.timedelta(seconds=round(video_length + 180)))}!\n", style="red")
inp = input("URL: ")
if not inp:
vidurl = "https://www.youtube.com/watch?v=n_Dv4JMiwK8"
else:
vidurl = inp
vidpath = vidurl.split("v=")[1]
if not Path(f"assets/mp4/{vidpath}.mp4").is_file():
def download_background():
"""Downloads the backgrounds/s video from youtube."""
Path("./assets/backgrounds/").mkdir(parents=True, exist_ok=True)
background_options = [ # uri , filename , credit
("https://www.youtube.com/watch?v=n_Dv4JMiwK8", "parkour.mp4", "bbswitzer"),
(
"https://www.youtube.com/watch?v=2X9QGY__0II",
"rocket_league.mp4",
"Orbital Gameplay",
),
]
# note: make sure the file name doesn't include a - in it
if len(listdir("./assets/backgrounds")) != len(
background_options
): # if there are any background videos not installed
print_step(
"We need to download the background video. This may be fairly large but it's only done once per background."
"We need to download the backgrounds videos. they are fairly large but it's only done once. 😎"
)
print_substep("Downloading the backgrounds videos... please be patient 🙏 ")
for uri, filename, credit in background_options:
print_substep(f"Downloading {filename} from {uri}")
YouTube(uri).streams.filter(res="1080p").first().download(
"assets/backgrounds", filename=f"{credit}-{filename}"
)
print_substep("Downloading the background video... please be patient.")
ydl_opts = {
"outtmpl": f"assets/mp4/{vidpath}.mp4",
"merge_output_format": "mp4",
}
with YoutubeDL(ydl_opts) as ydl:
ydl.download(vidurl)
print_substep(
"Background videos downloaded successfully! 🎉", style="bold green"
)
print_substep("Background video downloaded successfully!", style="bold green")
return vidpath
def chop_background_video(video_length):
print_step("Finding a spot in the backgrounds video to chop...✂️")
choice = random.choice(listdir("assets/backgrounds"))
environ["background_credit"] = choice.split("-")[0]
background = VideoFileClip(f"assets/backgrounds/{choice}")
def chop_background_video(video_length, vidpath):
print_step("Finding a spot in the background video to chop...")
background = VideoFileClip(f"assets/mp4/{vidpath}.mp4")
if background.duration < video_length + 180:
print_substep("This video is too short.", style="red")
noerror = False
return noerror
start_time, end_time = get_start_and_end_times(video_length, background.duration)
ffmpeg_extract_subclip(
f"assets/mp4/{vidpath}.mp4",
f"assets/backgrounds/{choice}",
start_time,
end_time,
targetname="assets/mp4/clip.mp4",
targetname="assets/temp/background.mp4",
)
print_substep("Background video chopped successfully!", style="bold green")
noerror = True

@ -0,0 +1,2 @@
videos.json
#todo add videos on github

@ -0,0 +1,14 @@
[
{
"name": "USER",
"value": "eyJwcmVmcyI6eyJ0b3BDb250ZW50RGlzbWlzc2FsVGltZSI6MCwiZ2xvYmFsVGhlbWUiOiJSRURESVQiLCJuaWdodG1vZGUiOnRydWUsImNvbGxhcHNlZFRyYXlTZWN0aW9ucyI6eyJmYXZvcml0ZXMiOmZhbHNlLCJtdWx0aXMiOmZhbHNlLCJtb2RlcmF0aW5nIjpmYWxzZSwic3Vic2NyaXB0aW9ucyI6ZmFsc2UsInByb2ZpbGVzIjpmYWxzZX0sInRvcENvbnRlbnRUaW1lc0Rpc21pc3NlZCI6MH19",
"domain": ".reddit.com",
"path": "/"
},
{
"name": "eu_cookie",
"value": "{%22opted%22:true%2C%22nonessential%22:false}",
"domain": ".reddit.com",
"path": "/"
}
]

@ -0,0 +1,8 @@
[
{
"name": "eu_cookie",
"value": "{%22opted%22:true%2C%22nonessential%22:false}",
"domain": ".reddit.com",
"path": "/"
}
]

@ -1,4 +1,9 @@
#!/usr/bin/env python3
import json
import os
import time
from os.path import exists
from moviepy.editor import (
VideoFileClip,
AudioFileClip,
@ -13,23 +18,25 @@ import re
from utils.console import print_step, print_substep
from dotenv import load_dotenv
import os
from moviepy.video.io import ffmpeg_tools
W, H = 1080, 1920
from reddit import subreddit
from utils.cleanup import cleanup
from utils.console import print_step, print_substep
from rich.console import Console
def make_final_video(number_of_clips):
console = Console()
# Calls opacity from the .env
load_dotenv()
opacity = os.getenv("OPACITY")
W, H = 1080, 1920
print_step("Creating the final video...")
def make_final_video(number_of_clips, length):
print_step("Creating the final video 🎥")
VideoFileClip.reW = lambda clip: clip.resize(width=W)
VideoFileClip.reH = lambda clip: clip.resize(width=H)
opacity = os.getenv("OPACITY")
background_clip = (
VideoFileClip("assets/mp4/clip.mp4")
VideoFileClip("assets/temp/background.mp4")
.without_audio()
.resize(height=H)
.crop(x1=1166.6, y1=0, x2=2246.6, y2=1920)
@ -38,12 +45,8 @@ def make_final_video(number_of_clips):
# 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("assets/mp3/title.mp3"))
try:
audio_clips.insert(1, AudioFileClip("assets/mp3/posttext.mp3"))
except:
OSError()
audio_clips.append(AudioFileClip(f"assets/temp/mp3/{i}.mp3"))
audio_clips.insert(0, AudioFileClip(f"assets/temp/mp3/title.mp3"))
audio_concat = concatenate_audioclips(audio_clips)
audio_composite = CompositeAudioClip([audio_concat])
@ -54,17 +57,37 @@ def make_final_video(number_of_clips):
# Output Length
console.log(f"[bold green] Video Will Be: {int_total_length} Seconds Long")
# Gather all images
image_clips = []
for i in range(0, number_of_clips):
if (
opacity is None or float(opacity) >= 1
): # opacity not set or is set to one OR MORE
image_clips.append(
ImageClip(f"assets/png/comment_{i}.png")
ImageClip(f"assets/temp/png/comment_{i}.png")
.set_duration(audio_clips[i + 1].duration)
.set_position("center")
.resize(width=W - 100),
)
else:
image_clips.append(
ImageClip(f"assets/temp/png/comment_{i}.png")
.set_duration(audio_clips[i + 1].duration)
.set_position("center")
.resize(width=W - 100)
.set_opacity(float(opacity)),
)
if (
opacity is None or float(opacity) >= 1
): # opacity not set or is set to one OR MORE
image_clips.insert(
0,
ImageClip(f"assets/temp/png/title.png")
.set_duration(audio_clips[0].duration)
.set_position("center")
.resize(width=W - 100)
.set_opacity(float(opacity)),
)
if os.path.exists("assets/mp3/posttext.mp3"):
image_clips.insert(
0,
@ -77,7 +100,7 @@ def make_final_video(number_of_clips):
else:
image_clips.insert(
0,
ImageClip("assets/png/title.png")
ImageClip("assets/temp/png/title.png")
.set_duration(audio_clips[0].duration)
.set_position("center")
.resize(width=W - 100)
@ -88,15 +111,50 @@ def make_final_video(number_of_clips):
)
image_concat.audio = audio_composite
final = CompositeVideoClip([background_clip, image_concat])
final_video_path = "assets/"
if os.getenv("FINAL_VIDEO_PATH"):
final_video_path = os.getenv("FINAL_VIDEO_PATH")
filename = (re.sub('[?\"%*:|<>]', '', (final_video_path + reddit.subreddit.submission.title + ".mp4")))
try:
final.write_videofile(filename, fps=30, audio_codec="aac", audio_bitrate="192k")
except:
print_substep("Something's wrong with the path you inserted, the video will be saved in the default path (assets/)", style="bold red")
filename = (re.sub('[?\"%*:|<>]', '', ("assets/" + reddit.subreddit.submission.title + ".mp4")))
final.write_videofile(filename, fps=30, audio_codec="aac", audio_bitrate="192k")
for i in range(0, number_of_clips):
pass
def get_video_title() -> str:
title = os.getenv("VIDEO_TITLE") or "final_video"
if len(title) <= 35:
return title
else:
return title[0:30] + "..."
filename = f"{get_video_title()}.mp4"
def save_data():
with open("./video_creation/data/videos.json", "r+") as raw_vids:
done_vids = json.load(raw_vids)
if str(subreddit.submission.id) in [video["id"] for video in done_vids]:
return # video already done but was specified to continue anyway in the .env file
payload = {
"id": str(os.getenv("VIDEO_ID")),
"time": str(int(time.time())),
"background_credit": str(os.getenv("background_credit")),
"reddit_title": str(os.getenv("VIDEO_TITLE")),
"filename": filename,
}
done_vids.append(payload)
raw_vids.seek(0)
json.dump(done_vids, raw_vids, ensure_ascii=False, indent=4)
save_data()
if not exists("./results"):
print_substep("the results folder didn't exist so I made it")
os.mkdir("./results")
final.write_videofile(
"assets/temp/temp.mp4", fps=30, audio_codec="aac", audio_bitrate="192k"
)
ffmpeg_tools.ffmpeg_extract_subclip(
"assets/temp/temp.mp4", 0, length, targetname=f"results/{filename}"
)
# os.remove("assets/temp/temp.mp4")
print_step("Removing temporary files 🗑")
cleanups = cleanup()
print_substep(f"Removed {cleanups} temporary files 🗑")
print_substep(f"See result in the results folder!")
print_step(
f"Reddit title: {os.getenv('VIDEO_TITLE')} \n Background Credit: {os.getenv('background_credit')}"
)

@ -1,16 +1,22 @@
#!/usr/bin/env python3
from playwright.sync_api import sync_playwright, ViewportSize
import json
from os import getenv
from pathlib import Path
from playwright.async_api import async_playwright
from playwright.sync_api import sync_playwright, ViewportSize
from rich.progress import track
from utils.console import print_step, print_substep
import json
from rich.console import Console
console = Console()
storymode = False
def download_screenshots_of_reddit_posts(reddit_object, screenshot_num, theme):
"""Downloads screenshots of reddit posts as they are seen on the web.
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.
@ -18,7 +24,7 @@ def download_screenshots_of_reddit_posts(reddit_object, screenshot_num, theme):
print_step("Downloading screenshots of reddit posts...")
# ! Make sure the reddit screenshots folder exists
Path("assets/png").mkdir(parents=True, exist_ok=True)
Path("assets/temp/png").mkdir(parents=True, exist_ok=True)
with sync_playwright() as p:
print_substep("Launching Headless Browser...")
@ -26,14 +32,12 @@ def download_screenshots_of_reddit_posts(reddit_object, screenshot_num, theme):
browser = p.chromium.launch()
context = browser.new_context()
try:
if theme.casefold() == "dark":
cookie_file = open('video_creation/cookies.json')
if getenv("THEME").upper() == "DARK":
cookie_file = open("./video_creation/data/cookie-dark-mode.json")
else:
cookie_file = open("./video_creation/data/cookie-light-mode.json")
cookies = json.load(cookie_file)
context.add_cookies(cookies)
except AttributeError:
pass
context.add_cookies(cookies) # load preference cookies
# Get the thread screenshot
page = context.new_page()
page.goto(reddit_object["thread_url"])
@ -43,19 +47,22 @@ def download_screenshots_of_reddit_posts(reddit_object, screenshot_num, theme):
print_substep("Post is NSFW. You are spicy...")
page.locator('[data-testid="content-gate"] button').click()
page.locator('[data-click-id="text"] button').click() # Remove "Click to see nsfw" Button in Screenshot
page.locator(
'[data-click-id="text"] button'
).click() # Remove "Click to see nsfw" Button in Screenshot
page.locator('[data-test-id="post-content"]').screenshot(
path="assets/png/title.png"
path="assets/temp/png/title.png"
)
if storymode:
page.locator('[data-click-id="text"]').screenshot(
path="assets/temp/png/story_content.png"
)
else:
for idx, comment in track(
enumerate(reddit_object["comments"])
enumerate(reddit_object["comments"]), "Downloading screenshots..."
):
#allow user to see what comment is being saved
print_substep(f"Downloading screenshot {idx + 1}")
# Stop if we have reached the screenshot_num
if idx >= screenshot_num:
break
@ -65,9 +72,6 @@ def download_screenshots_of_reddit_posts(reddit_object, screenshot_num, theme):
page.goto(f'https://reddit.com{comment["comment_url"]}')
page.locator(f"#t1_{comment['comment_id']}").screenshot(
path=f"assets/png/comment_{idx}.png"
path=f"assets/temp/png/comment_{idx}.png"
)
#let user know that the screenshots are done
console.log(f"[bold green]Saved {idx + 1} screenshots.")
print_substep("Screenshots downloaded Successfully.", style="bold green")

@ -1,16 +1,26 @@
#!/usr/bin/env python3
from gtts import gTTS
from pathlib import Path
from mutagen.mp3 import MP3
from utils.console import print_step, print_substep
from os import getenv, name
import sox
from mutagen import MutagenError
from mutagen.mp3 import MP3, HeaderNotFoundError
from rich.progress import track
from rich.console import Console
console = Console()
import re
from utils.console import print_step, print_substep
from utils.voice import sanitize_text
from video_creation.TTSwrapper import TTTTSWrapper
VIDEO_LENGTH: int = 40 # secs
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.
"""
@ -18,37 +28,52 @@ def save_text_to_mp3(reddit_obj):
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)
tts.save("assets/mp3/title.mp3")
length += MP3("assets/mp3/title.mp3").info.length
Path("assets/temp/mp3").mkdir(parents=True, exist_ok=True)
ttttsw = TTTTSWrapper() # tiktok text to speech wrapper
ttttsw.tts(
sanitize_text(reddit_obj["thread_title"]),
filename=f"assets/temp/mp3/title.mp3",
random_speaker=False,
)
try:
Path("assets/mp3/posttext.mp3").unlink()
except OSError:
pass
if reddit_obj["thread_post"] != "":
tts = gTTS(text=reddit_obj["thread_post"], lang="en", slow=False)
tts.save("assets/mp3/posttext.mp3")
length += MP3("assets/mp3/posttext.mp3").info.length
for idx, comment in track(enumerate(reddit_obj["comments"])):
length += MP3(f"assets/temp/mp3/title.mp3").info.length
except HeaderNotFoundError: # note to self AudioFileClip
length += sox.file_info.duration(f"assets/temp/mp3/title.mp3")
if getenv("STORYMODE").casefold() == "true":
ttttsw.tts(
sanitize_text(reddit_obj["thread_content"]),
filename=f"assets/temp/mp3/story_content.mp3",
random_speaker=False,
)
#'story_content'
com = 0
for comment in track((reddit_obj["comments"]), "Saving..."):
# ! Stop creating mp3 files if the length is greater than VIDEO_LENGTH seconds. This can be longer, but this is just a good_voices starting point
if length > VIDEO_LENGTH:
break
#allow user to see what comment is being saved
print_substep(f"Saving MP3 {idx + 1} ")
ttttsw.tts(
sanitize_text(comment["comment_body"]),
filename=f"assets/temp/mp3/{com}.mp3",
random_speaker=False,
)
try:
length += MP3(f"assets/temp/mp3/{com}.mp3").info.length
com += 1
except (HeaderNotFoundError, MutagenError, Exception):
try:
length += sox.file_info.duration(f"assets/temp/mp3/{com}.mp3")
com += 1
except (OSError, IOError):
print(
"would have removed"
f"assets/temp/mp3/{com}.mp3"
f"assets/temp/png/comment_{com}.png"
)
# remove(f"assets/temp/mp3/{com}.mp3")
# remove(f"assets/temp/png/comment_{com}.png")# todo might cause odd un-syncing
# ! 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
comment=comment["comment_body"]
text=re.sub('((http|https)\:\/\/)?[a-zA-Z0-9\.\/\?\:@\-_=#]+\.([a-zA-Z]){2,6}([a-zA-Z0-9\.\&\/\?\:@\-_=#])*', '', comment)
tts = gTTS(text, lang="en", slow=False)
tts.save(f"assets/mp3/{idx}.mp3")
length += MP3(f"assets/mp3/{idx}.mp3").info.length
#let user know that the MP3 files are saved
console.log(f"[bold green]Saved {idx + 1} MP3 Files.")
# ! Return the index so we know how many screenshots of comments we need to make.
return length, idx
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, com

Loading…
Cancel
Save