Merge branch 'develop' into patch-1

pull/465/head
micziz 3 years ago committed by GitHub
commit 5e98716768
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,3 @@
# This can be found in the email that reddit sent you when you created the app
REDDIT_CLIENT_ID="" REDDIT_CLIENT_ID=""
REDDIT_CLIENT_SECRET="" REDDIT_CLIENT_SECRET=""
@ -6,16 +5,8 @@ REDDIT_USERNAME=""
REDDIT_PASSWORD="" REDDIT_PASSWORD=""
# If no, it will ask you a thread link to extract the thread, if yes it will randomize it. Default: "no" # If no, it will ask you a thread link to extract the thread, if yes it will randomize it. Default: "no"
RANDOM_THREAD="" RANDOM_THREAD="no"
# 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"
# 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 # Valid options are "yes" and "no" for the variable below
REDDIT_2FA="" REDDIT_2FA=""
SUBREDDIT="AskReddit" SUBREDDIT="AskReddit"
@ -25,14 +16,16 @@ ALLOW_NSFW="False"
POST_ID="" POST_ID=""
#set to either LIGHT or DARK #set to either LIGHT or DARK
THEME="LIGHT" THEME="LIGHT"
# used if you want to run multiple times. set to an int e.g. 4 or 29 and leave blank for once # used if you want to run multiple times. set to an int e.g. 4 or 29 and leave blank for 1
TIMES_TO_RUN="" TIMES_TO_RUN=""
MAX_COMMENT_LENGTH="500" # max number of characters a comment can have.
MAX_COMMENT_LENGTH="500" # default is 500
# Range is 0 -> 1 recommended around 0.8-0.9 # Range is 0 -> 1 recommended around 0.8-0.9
OPACITY="1" OPACITY="1"
# see TTSwrapper.py for all valid options # see different voice options: todo: add docs
VOICE="en_us_001" # e.g. en_us_002 VOICE="Matthew" # e.g. en_us_002
TTsChoice="polly"
# IN-PROGRESS - not yet implemented # IN-PROGRESS - not yet implemented
STORYMODE="False" STORYMODE="False"

1
.gitattributes vendored

@ -0,0 +1 @@
* text=auto eol=lf

@ -5,7 +5,7 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: "pip" # See documentation for possible values - package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests directory: "/" # Location of package manifests
schedule: schedule:
interval: "daily" interval: "daily"

@ -12,61 +12,61 @@
name: "CodeQL" name: "CodeQL"
on: on:
push: push:
branches: [ "master" ] branches: [ "master" ]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ "master" ] branches: [ "master" ]
schedule: schedule:
- cron: '16 14 * * 3' - cron: '16 14 * * 3'
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
actions: read actions: read
contents: read contents: read
security-events: write security-events: write
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
language: [ 'python' ] language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # 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. # 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. # 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 # 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 # queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # 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) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # 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 # 📚 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. # 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. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: | # - run: |
# echo "Run, Build Application using script" # echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v2

87
.gitignore vendored

@ -153,20 +153,91 @@ dmypy.json
cython_debug/ cython_debug/
# PyCharm # PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # User-specific stuff
.idea/ .idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
assets/ assets/
out out
.DS_Store .DS_Store
.setup-done-before .setup-done-before
assets/
results/* results/*
.env
reddit-bot-351418-5560ebc49cac.json reddit-bot-351418-5560ebc49cac.json
/.idea /.idea
*.pyc *.pyc
/video_creation/data/videos.json video_creation/data/videos.json
video_creation/data/envvars.txt

@ -8,16 +8,22 @@ All types of contributions are encouraged and valued. See the [Table of Contents
> >
> - ⭐ Star the project > - ⭐ Star the project
> - 📣 Tweet about it > - 📣 Tweet about it
> - 🌲 Refer this project in your project's readme > - 🌲 Refer this project in your project's readme
## Table of Contents ## Table of Contents
- [I Have a Question](#i-have-a-question) - [Contributing to Reddit Video Maker Bot 🎥](#contributing-to-reddit-video-maker-bot-)
- [I Want To Contribute](#i-want-to-contribute) - [Table of Contents](#table-of-contents)
- [Reporting Bugs](#reporting-bugs) - [I Have a Question](#i-have-a-question)
- [Suggesting Enhancements](#suggesting-enhancements) - [I Want To Contribute](#i-want-to-contribute)
- [Your First Code Contribution](#your-first-code-contribution) - [Reporting Bugs](#reporting-bugs)
- [Improving The Documentation](#improving-the-documentation) - [How Do I Submit a Good Bug Report?](#how-do-i-submit-a-good-bug-report)
- [Suggesting Enhancements](#suggesting-enhancements)
- [How Do I Submit a Good Enhancement Suggestion?](#how-do-i-submit-a-good-enhancement-suggestion)
- [Your First Code Contribution](#your-first-code-contribution)
- [Your environment](#your-environment)
- [Making your first PR](#making-your-first-pr)
- [Improving The Documentation](#improving-the-documentation)
## I Have a Question ## I Have a Question
@ -38,6 +44,7 @@ Additionally, there is a [Discord Server](https://discord.gg/swqtb7AsNQ) for any
## I Want To Contribute ## I Want To Contribute
### Reporting Bugs ### Reporting Bugs
<details><summary><h4>Before Submitting a Bug Report</h4></summary> <details><summary><h4>Before Submitting a Bug Report</h4></summary>
A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
@ -51,9 +58,9 @@ A good bug report shouldn't leave others needing to chase you up for more inform
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM) - OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Your input and the output - Your input and the output
- Is the issue reproducable? Does it exist in previous versions? - Is the issue reproducible? Does it exist in previous versions?
</details>
<details><summary><h4>How Do I Submit a Good Bug Report?</h4></summary> #### How Do I Submit a Good Bug Report?
We use GitHub issues to track bugs and errors. If you run into an issue with the project: We use GitHub issues to track bugs and errors. If you run into an issue with the project:
@ -65,7 +72,7 @@ We use GitHub issues to track bugs and errors. If you run into an issue with the
Once it's filed: Once it's filed:
- The project team will label the issue accordingly. - The project team will label the issue accordingly.
- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will try to support you as best as they can, but you may not recieve an instant. - A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will try to support you as best as they can, but you may not receive an instant.
- If the team discovers that this is an issue it will be marked `bug` or `error`, as well as possibly other tags relating to the nature of the error), and the issue will be left to be [implemented by someone](#your-first-code-contribution). - If the team discovers that this is an issue it will be marked `bug` or `error`, as well as possibly other tags relating to the nature of the error), and the issue will be left to be [implemented by someone](#your-first-code-contribution).
</details> </details>

@ -4,9 +4,19 @@ All done WITHOUT video editing or asset compiling. Just pure ✨programming magi
Created by Lewis Menelaws & [TMRRW](https://tmrrwinc.ca) Created by Lewis Menelaws & [TMRRW](https://tmrrwinc.ca)
<a target="_blank" href="https://tmrrwinc.ca">
<picture>
<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: 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"> <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" alt="img"> <img src="https://user-images.githubusercontent.com/6053155/170528582-cb6671e7-5a2f-4bd4-a048-0e6cfa54f0f7.png" width="350">
</picture>
</a>
## Video Explainer
[![lewisthumbnail](https://user-images.githubusercontent.com/6053155/173631669-1d1b14ad-c478-4010-b57d-d79592a789f2.png)
](https://www.youtube.com/watch?v=3gjcY_00U1w)
## Motivation 🤔 ## Motivation 🤔
@ -29,26 +39,27 @@ The only original thing being done is the editing and gathering of all materials
## Installation 👩‍💻 ## Installation 👩‍💻
1. Clone this repository 1. Clone this repository
2. Run `pip3 install -r requirements.txt`
3. Run `playwright install` and `playwright install-deps`.
4. Install [SoX](https://sourceforge.net/projects/sox/files/sox/)
**EXPERIMENTAL**: Run this install script to do steps 1-3 automatically (it also install dependencies!). Supports MacOS and Debian, Arch, CentoOS and fedora. 2. Install [SoX](https://sourceforge.net/projects/sox/files/sox/)
3. Run `pip install -r requirements.txt`
4. Run `playwright install` and `playwright install-deps`. (if this fails try adding python -m to the front of the command)
**EXPERIMENTAL**: Run this install script to do steps 1-4 automatically (it also install dependencies!). Supports MacOS and Debian, Arch, CentoOS and fedora.
To run: `sh <(curl -sL https://raw.githubusercontent.com/micziz/RedditVideoMakerBot/master/install.sh)` To run: `sh <(curl -sL https://raw.githubusercontent.com/micziz/RedditVideoMakerBot/master/install.sh)`
5.
5a **Automatic Install**: Run `python3 main.py` and type 'yes' to activate the setup assistant. 6. Run `python main.py` (unless you chose automatic install, then the installer will automatically run main.py)
5b **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.
6. 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". 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. Copy your keys into the `.env` file, along with whether your account uses two-factor authentication.
7. Enjoy 😎 7. Enjoy 😎
(Note if you got an error installing or running the bot try first rerunning the command with a three after the name e.g. python3 or pip3)
## Video ## Video
https://user-images.githubusercontent.com/66544866/173453972-6526e4e6-c6ef-41c5-ab40-5d275e724e7c.mp4 https://user-images.githubusercontent.com/66544866/173453972-6526e4e6-c6ef-41c5-ab40-5d275e724e7c.mp4
## Contributing & Ways to improve 📈 ## Contributing & Ways to improve 📈
In its current state, this bot does exactly what it needs to do. However, lots of improvements can be made. In its current state, this bot does exactly what it needs to do. However, lots of improvements can be made.
@ -62,7 +73,7 @@ I have tried to simplify the code so anyone can read it and start contributing a
- [x] Allowing users to change voice. - [x] Allowing users to change voice.
- [x] Checks if a video has already been created - [x] Checks if a video has already been created
- [x] Light and Dark modes - [x] Light and Dark modes
- [x] Nsfw post filter - [x] NSFW post filter
Please read our [contributing guidelines](CONTRIBUTING.md) for more detailed information. Please read our [contributing guidelines](CONTRIBUTING.md) for more detailed information.
@ -72,10 +83,10 @@ Elebumm (Lewis#6305) - https://github.com/elebumm (Founder)
Jason (JasonLovesDoggo#1904) - https://github.com/JasonLovesDoggo Jason (JasonLovesDoggo#1904) - https://github.com/JasonLovesDoggo
CallumIO - https://github.com/CallumIO CallumIO (c.#6837) - https://github.com/CallumIO
HarryDaDev (hrvyy#9677) - https://github.com/ImmaHarry HarryDaDev (hrvyy#9677) - https://github.com/ImmaHarry
LukaHietala (Pix.#0001) - https://github.com/LukaHietala LukaHietala (Pix.#0001) - https://github.com/LukaHietala
Freebiell - https://github.com/FreebieII Freebiell (Freebie#6429) - https://github.com/FreebieII

@ -0,0 +1,13 @@
from gtts import gTTS
class GTTS:
def tts(
self,
req_text: str = "Google Text To Speech",
filename: str = "title.mp3",
random_speaker=False,
censor=False,
):
tts = gTTS(text=req_text, lang="en", slow=False)
tts.save(f"{filename}")

@ -0,0 +1,106 @@
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.exceptions import JSONDecodeError
voices = [
"Brian",
"Emma",
"Russell",
"Joey",
"Matthew",
"Joanna",
"Kimberly",
"Amy",
"Geraint",
"Nicole",
"Justin",
"Ivy",
"Kendra",
"Salli",
"Raveena",
]
# valid voices https://lazypy.ro/tts/
class POLLY:
def __init__(self):
self.url = "https://streamlabs.com/polly/speak"
def tts(
self,
req_text: str = "Amazon Text To Speech",
filename: str = "title.mp3",
random_speaker=False,
censor=False,
):
if random_speaker:
voice = self.randomvoice()
else:
if not os.getenv("VOICE"):
return ValueError(
"Please set the environment variable VOICE to a valid voice. options are: {}".format(
voices
)
)
voice = str(os.getenv("VOICE")).capitalize()
body = {"voice": voice, "text": req_text, "service": "polly"}
response = requests.post(self.url, data=body)
try:
voice_data = requests.get(response.json()["speak_url"])
with open(filename, "wb") as f:
f.write(voice_data.content)
except (KeyError, JSONDecodeError):
if response.json()["error"] == "Text length is too long!":
chunks = [m.group().strip() for m in re.finditer(r" *((.{0,499})(\.|.$))", req_text)]
audio_clips = []
cbn = sox.Combiner()
chunkId = 0
for chunk in chunks:
body = {"voice": voice, "text": chunk, "service": "polly"}
resp = requests.post(self.url, data=body)
voice_data = requests.get(resp.json()["speak_url"])
with open(filename.replace(".mp3", f"-{chunkId}.mp3"), "wb") as out:
out.write(voice_data.content)
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,
FileNotFoundError,
): # 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)
def make_readable(self, text):
"""
Amazon Polly fails to read some symbols properly such as '& (and)'.
So we normalize input text before passing it to the service
"""
text = text.replace("&", "and")
return text
def randomvoice(self):
return random.choice(voices)

@ -65,41 +65,37 @@ noneng = [
# 'ok': ['en_au_002', 'en_uk_001']} # less en_us_stormtrooper more less en_us_rocket en_us_ghostface # '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 class TikTok: # TikTok Text-to-Speech Wrapper
def __init__(self): def __init__(self):
self.URI_BASE = "https://api16-normal-useast5.us.tiktokv.com/media/api/text/speech/invoke/?text_speaker=" self.URI_BASE = (
"https://api16-normal-useast5.us.tiktokv.com/media/api/text/speech/invoke/?text_speaker="
)
def tts( def tts(
self, self,
req_text: str = "TikTok Text To Speech", req_text: str = "TikTok Text To Speech",
filename: str = "title.mp3", filename: str = "title.mp3",
random_speaker: bool = False, random_speaker: bool = False,
censer=False, censor=False,
): ):
req_text = req_text.replace("+", "plus").replace(" ", "+").replace("&", "and") req_text = req_text.replace("+", "plus").replace(" ", "+").replace("&", "and")
if censer: if censor:
# req_text = pf.censor(req_text) # req_text = pf.censor(req_text)
pass pass
voice = ( voice = (
self.randomvoice() self.randomvoice() if random_speaker else (os.getenv("VOICE") or random.choice(human))
if random_speaker
else (os.getenv("VOICE") or random.choice(human))
) )
chunks = [ chunks = [m.group().strip() for m in re.finditer(r" *((.{0,299})(\.|.$))", req_text)]
m.group().strip() for m in re.finditer(r" *((.{0,299})(\.|.$))", req_text)
]
audio_clips = [] audio_clips = []
cbn = sox.Combiner() cbn = sox.Combiner()
#cbn.set_input_format(file_type=["mp3" for _ in chunks]) # cbn.set_input_format(file_type=["mp3" for _ in chunks])
chunkId = 0 chunkId = 0
for chunk in chunks: for chunk in chunks:
try: try:
r = requests.post( r = requests.post(f"{self.URI_BASE}{voice}&req_text={chunk}&speaker_map_type=0")
f"{self.URI_BASE}{voice}&req_text={chunk}&speaker_map_type=0"
)
except requests.exceptions.SSLError: except requests.exceptions.SSLError:
# https://stackoverflow.com/a/47475019/18516611 # https://stackoverflow.com/a/47475019/18516611
session = requests.Session() session = requests.Session()
@ -107,9 +103,8 @@ class TTTTSWrapper: # TikTok Text-to-Speech Wrapper
adapter = HTTPAdapter(max_retries=retry) adapter = HTTPAdapter(max_retries=retry)
session.mount("http://", adapter) session.mount("http://", adapter)
session.mount("https://", adapter) session.mount("https://", adapter)
r = session.post( r = session.post(f"{self.URI_BASE}{voice}&req_text={chunk}&speaker_map_type=0")
f"{self.URI_BASE}{voice}&req_text={chunk}&speaker_map_type=0" print(r.text)
)
vstr = [r.json()["data"]["v_str"]][0] vstr = [r.json()["data"]["v_str"]][0]
b64d = base64.b64decode(vstr) b64d = base64.b64decode(vstr)
@ -125,12 +120,14 @@ class TTTTSWrapper: # TikTok Text-to-Speech Wrapper
cbn.build(audio_clips, filename, "concatenate") cbn.build(audio_clips, filename, "concatenate")
else: else:
os.rename(audio_clips[0], filename) os.rename(audio_clips[0], filename)
except (
except sox.core.SoxError: # https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/67#issuecomment-1150466339 sox.core.SoxError,
FileNotFoundError,
): # https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/67#issuecomment-1150466339
for clip in audio_clips: for clip in audio_clips:
i = audio_clips.index(clip) # get the index of the clip i = audio_clips.index(clip) # get the index of the clip
audio_clips = ( audio_clips = (
audio_clips[:i] + [AudioFileClip(clip)] + audio_clips[i + 1:] audio_clips[:i] + [AudioFileClip(clip)] + audio_clips[i + 1 :]
) # replace the clip with an AudioFileClip ) # replace the clip with an AudioFileClip
audio_concat = concatenate_audioclips(audio_clips) audio_concat = concatenate_audioclips(audio_clips)
audio_composite = CompositeAudioClip([audio_concat]) audio_composite = CompositeAudioClip([audio_concat])

@ -0,0 +1,24 @@
from os import getenv
from dotenv import load_dotenv
from TTS.GTTS import GTTS
from TTS.POLLY import POLLY
from TTS.TikTok import TikTok
from utils.console import print_substep
CHOICE_DIR = {"tiktok": TikTok, "gtts": GTTS, "polly": POLLY}
class TTS:
def __new__(cls):
load_dotenv()
try:
CHOICE = getenv("TTsChoice").casefold()
except AttributeError:
print_substep("None defined. Defaulting to 'polly.'")
CHOICE = "polly"
valid_keys = [key.lower() for key in CHOICE_DIR.keys()]
if CHOICE not in valid_keys:
raise ValueError(f"{CHOICE} is not valid. Please use one of these {valid_keys} options")
return CHOICE_DIR.get(CHOICE)()

@ -6,24 +6,26 @@ from os import getenv, name
from reddit.subreddit import get_subreddit_threads from reddit.subreddit import get_subreddit_threads
from utils.cleanup import cleanup from utils.cleanup import cleanup
from utils.console import print_markdown, print_step from utils.console import print_markdown, print_step
from utils.checker import envUpdate
from video_creation.background import download_background, chop_background_video from video_creation.background import download_background, chop_background_video
from video_creation.final_video import make_final_video from video_creation.final_video import make_final_video
from video_creation.screenshot_downloader import download_screenshots_of_reddit_posts from video_creation.screenshot_downloader import download_screenshots_of_reddit_posts
from video_creation.voices import save_text_to_mp3 from video_creation.voices import save_text_to_mp3
VERSION = 2.1
banner = """ print(
"""
""" """
print(banner) )
load_dotenv() load_dotenv()
# Modified by JasonLovesDoggo # Modified by JasonLovesDoggo
print_markdown( 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." "### 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. You can find solutions to many common problems in the [Documentation](https://luka-hietala.gitbook.io/documentation-for-the-reddit-bot/)"
) )
time.sleep(1) time.sleep(1)
@ -36,6 +38,7 @@ reddit2fa = getenv("REDDIT_2FA")
def main(): def main():
envUpdate()
cleanup() cleanup()
def get_obj(): def get_obj():
@ -47,7 +50,7 @@ def main():
download_screenshots_of_reddit_posts(reddit_object, number_of_comments) download_screenshots_of_reddit_posts(reddit_object, number_of_comments)
download_background() download_background()
chop_background_video(length) chop_background_video(length)
final_video = make_final_video(number_of_comments, length) make_final_video(number_of_comments, length)
def run_many(times): def run_many(times):

@ -1,3 +1,4 @@
import re
from os import getenv, environ from os import getenv, environ
import praw import praw
@ -14,6 +15,13 @@ def textify(text):
return "".join(filter(TEXT_WHITELIST.__contains__, text)) return "".join(filter(TEXT_WHITELIST.__contains__, text))
def try_env(param, backup):
try:
return environ[param]
except KeyError:
return backup
def get_subreddit_threads(): def get_subreddit_threads():
""" """
Returns a list of threads from the AskReddit subreddit. Returns a list of threads from the AskReddit subreddit.
@ -23,9 +31,7 @@ def get_subreddit_threads():
content = {} content = {}
if str(getenv("REDDIT_2FA")).casefold() == "yes": if str(getenv("REDDIT_2FA")).casefold() == "yes":
print( print("\nEnter your two-factor authentication code from your authenticator app.\n")
"\nEnter your two-factor authentication code from your authenticator app.\n"
)
code = input("> ") code = input("> ")
print() print()
pw = getenv("REDDIT_PASSWORD") pw = getenv("REDDIT_PASSWORD")
@ -41,19 +47,22 @@ def get_subreddit_threads():
check_for_async=False, check_for_async=False,
) )
""" """
Ask user for subreddit input Ask user for subreddit input
""" """
print_step("Getting subreddit threads...") print_step("Getting subreddit threads...")
if not getenv( if not getenv(
"SUBREDDIT" "SUBREDDIT"
): # note to self. you can have multiple subreddits via reddit.subreddit("redditdev+learnpython") ): # note to user. you can have multiple subreddits via reddit.subreddit("redditdev+learnpython")
subreddit = reddit.subreddit( try:
input("What subreddit would you like to pull from? ") subreddit = reddit.subreddit(
) # if the env isnt set, ask user re.sub(r"r\/", "", input("What subreddit would you like to pull from? "))
# removes the r/ from the input
)
except ValueError:
subreddit = reddit.subreddit("askreddit")
print_substep("Subreddit not defined. Using AskReddit.")
else: else:
print_substep( print_substep(f"Using subreddit: r/{getenv('SUBREDDIT')} from environment variable config")
f"Using subreddit: r/{getenv('SUBREDDIT')} from environment variable config"
)
subreddit = reddit.subreddit( subreddit = reddit.subreddit(
getenv("SUBREDDIT") getenv("SUBREDDIT")
) # Allows you to specify in .env. Done for automation purposes. ) # Allows you to specify in .env. Done for automation purposes.
@ -71,14 +80,10 @@ def get_subreddit_threads():
num_comments = submission.num_comments num_comments = submission.num_comments
print_substep(f"Video will be: {submission.title} :thumbsup:", style="bold green") 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 {upvotes} upvotes", style="bold blue")
print_substep( print_substep(f"Thread has a upvote ratio of {ratio}%", style="bold blue")
f"Thread has a upvote ratio of " + str(ratio) + "%", style="bold blue" print_substep(f"Thread has {num_comments} comments", style="bold blue")
) environ["VIDEO_TITLE"] = str(textify(submission.title)) # todo use global instend of env vars
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)) environ["VIDEO_ID"] = str(textify(submission.id))
content["thread_url"] = f"https://reddit.com{submission.permalink}" content["thread_url"] = f"https://reddit.com{submission.permalink}"
@ -91,7 +96,7 @@ def get_subreddit_threads():
if top_level_comment.body in ["[removed]", "[deleted]"]: if top_level_comment.body in ["[removed]", "[deleted]"]:
continue # # see https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/78 continue # # see https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/78
if not top_level_comment.stickied: if not top_level_comment.stickied:
if len(top_level_comment.body) <= int(environ["MAX_COMMENT_LENGTH"]): if len(top_level_comment.body) <= int(try_env("MAX_COMMENT_LENGTH", 500)):
content["comments"].append( content["comments"].append(
{ {
"comment_body": top_level_comment.body, "comment_body": top_level_comment.body,

@ -40,14 +40,10 @@ def handle_input(
except ValueError: except ValueError:
console.log("[red]" + err_message) # Type conversion failed console.log("[red]" + err_message) # Type conversion failed
continue continue
if ( if nmin is not None and len(user_input) < nmin: # Check if string is long enough
nmin is not None and len(user_input) < nmin
): # Check if string is long enough
console.log("[red]" + oob_error) console.log("[red]" + oob_error)
continue continue
if ( if nmax is not None and len(user_input) > nmax: # Check if string is not too long
nmax is not None and len(user_input) > nmax
): # Check if string is not too long
console.log("[red]" + oob_error) console.log("[red]" + oob_error)
continue continue
break break

@ -0,0 +1,67 @@
import os
import subprocess
import tempfile
from os import path
from sys import platform
ACCEPTABLE_TO_BE_LEFT_BLANK = ["RANDOM_THREAD", "TIMES_TO_RUN"]
def envUpdate():
if path.exists(".env.template"): # if .env.template exists and .env does not exist
if platform == "win32" or platform == "cygwin":
runPS("utils/scripts/FileGrabber.ps1")
with open(".\\video_creation\\data\\envvars.txt", "rb") as f:
envTemplate = f.read()
elif platform == "darwin" or platform == "linux":
envTemplate = subprocess.check_output(
"awk -F '=' 'NF {print $1}' .env.template | grep --regexp=^[a-zA-Z]",
shell=True,
)
else:
raise OSError("Unsupported platform")
elif path.exists(".env"):
if platform == "win32" or platform == "cygwin":
runPS("utils/scripts/FileGrabberenv.ps1")
with open(".\\video_creation\\data\\envvars.txt", "rb") as f:
envTemplate = f.read()
elif platform == "darwin" or platform == "linux":
envTemplate = subprocess.check_output(
"awk -F '=' 'NF {print $1}' .env | grep --regexp=^[a-zA-Z]",
shell=True,
)
else:
raise OSError("Unsupported platform")
else:
raise FileNotFoundError("No .env or .env.template file found")
tempEnv = tempfile.TemporaryFile()
tempEnv.write(envTemplate)
tempEnv.seek(0)
envVars = tempEnv.readlines()
missing = []
isMissingEnvs = False
for env in envVars:
try:
env = env.decode("utf-8").strip()
except AttributeError:
env = env.strip()
if env not in os.environ:
if str(env) in ACCEPTABLE_TO_BE_LEFT_BLANK:
continue
isMissingEnvs = True
missing.append(env)
if isMissingEnvs:
printstr = ""
[printstr + str(var) for var in missing]
print(
f"The following environment variables are missing: {printstr}. Please add them to the .env file."
)
exit(-1)
def runPS(cmd):
completed = subprocess.run(["powershell", "-Command", cmd], capture_output=True)
return completed

@ -5,9 +5,7 @@ from os.path import exists
def cleanup() -> int: def cleanup() -> int:
if exists("./assets/temp"): if exists("./assets/temp"):
count = 0 count = 0
files = [ files = [f for f in os.listdir(".") if f.endswith(".mp4") and "temp" in f.lower()]
f for f in os.listdir(".") if f.endswith(".mp4") and "temp" in f.lower()
]
count += len(files) count += len(files)
for f in files: for f in files:
os.remove(f) os.remove(f)

@ -0,0 +1,40 @@
# write a class that takes .env file and parses it into a dictionary
from dotenv import dotenv_values
DEFAULTS = {
"SUBREDDIT": "AskReddit",
"ALLOW_NSFW": "False",
"POST_ID": "",
"THEME": "DARK",
"REDDIT_2FA": "no",
"TIMES_TO_RUN": "",
"MAX_COMMENT_LENGTH": "500",
"OPACITY": "1",
"VOICE": "en_us_001",
"STORYMODE": "False",
}
class Config:
def __init__(self):
self.raw = dotenv_values("../.env")
self.load_attrs()
def __getattr__(self, attr): # code completion for attributes fix.
return getattr(self, attr)
def load_attrs(self):
for key, value in self.raw.items():
self.add_attr(key, value)
def add_attr(self, key, value):
if value is None or value == "":
setattr(self, key, DEFAULTS[key])
else:
setattr(self, key, str(value))
config = Config()
print(config.SUBREDDIT)

@ -1,4 +1,3 @@
# Okay, have to admit. This code is from StackOverflow. It's so efficient, that it's probably the best way to do it. # Okay, have to admit. This code is from StackOverflow. It's so efficient, that it's probably the best way to do it.
# Although, it is edited to use less threads. # Although, it is edited to use less threads.
@ -15,9 +14,9 @@ class Loader:
A loader-like context manager A loader-like context manager
Args: Args:
desc (str, optional): The loader's description. Defaults to "Loading...". desc (str, optional): The loader's description. Defaults to "Loading...".
end (str, optional): Final print. Defaults to "Done!". end (str, optional): Final print. Defaults to "Done!".
timeout (float, optional): Sleep time between prints. Defaults to 0.1. timeout (float, optional): Sleep time between prints. Defaults to 0.1.
""" """
self.desc = desc self.desc = desc
self.end = end self.end = end

@ -0,0 +1,9 @@
$envFile = Get-Content ".\.env.template"
$envFile -split "=" | Where-Object {$_ -notmatch '\"'} | Set-Content ".\envVarsbefSpl.txt"
Get-Content ".\envVarsbefSpl.txt" | Where-Object {$_ -notmatch '\#'} | Set-Content ".\envVarsN.txt"
Get-Content ".\envVarsN.txt" | Where-Object {$_ -ne ''} | Set-Content ".\video_creation\data\envvars.txt"
Remove-Item ".\envVarsbefSpl.txt"
Remove-Item ".\envVarsN.txt"
Write-Host $nowSplit

@ -0,0 +1,9 @@
$envFile = Get-Content ".\.env"
$envFile -split "=" | Where-Object {$_ -notmatch '\"'} | Set-Content ".\envVarsbefSpl.txt"
Get-Content ".\envVarsbefSpl.txt" | Where-Object {$_ -notmatch '\#'} | Set-Content ".\envVarsN.txt"
Get-Content ".\envVarsN.txt" | Where-Object {$_ -ne ''} | Set-Content ".\video_creation\data\envvars.txt"
Remove-Item ".\envVarsbefSpl.txt"
Remove-Item ".\envVarsN.txt"
Write-Host $nowSplit

@ -3,6 +3,7 @@ import json
from os import getenv from os import getenv
from utils.console import print_substep from utils.console import print_substep
def get_subreddit_undone(submissions: List, subreddit): def get_subreddit_undone(submissions: List, subreddit):
""" """
recursively checks if the top submission in the list was already done. recursively checks if the top submission in the list was already done.
@ -13,10 +14,14 @@ def get_subreddit_undone(submissions: List, subreddit):
if already_done(done_videos, submission): if already_done(done_videos, submission):
continue continue
if submission.over_18: if submission.over_18:
if getenv("ALLOW_NSFW").casefold() == "false": try:
print_substep("NSFW Post Detected. Skipping...") if getenv("ALLOW_NSFW").casefold() == "false":
continue print_substep("NSFW Post Detected. Skipping...")
continue
except AttributeError:
print_substep("NSFW settings not defined. Skipping NSFW post...")
return submission return submission
print("all submissions have been done going by top submission order")
return get_subreddit_undone( return get_subreddit_undone(
subreddit.top(time_filter="hour"), subreddit subreddit.top(time_filter="hour"), subreddit
) # all of the videos in hot have already been done ) # all of the videos in hot have already been done

@ -4,7 +4,9 @@ from os import getenv
from utils.console import print_step 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 def check_done(
redditobj,
): # don't set this to be run anyplace that isn't subreddit.py bc of inspect stack
"""params: """params:
reddit_object: The Reddit Object you received in askreddit.py""" reddit_object: The Reddit Object you received in askreddit.py"""
with open("./video_creation/data/videos.json", "r") as done_vids_raw: with open("./video_creation/data/videos.json", "r") as done_vids_raw:

@ -7,8 +7,6 @@ from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
from moviepy.editor import VideoFileClip from moviepy.editor import VideoFileClip
from utils.console import print_step, print_substep from utils.console import print_step, print_substep
import datetime
def get_start_and_end_times(video_length, length_of_clip): def get_start_and_end_times(video_length, length_of_clip):
random_time = randrange(180, int(length_of_clip) - int(video_length)) random_time = randrange(180, int(length_of_clip) - int(video_length))
@ -16,18 +14,18 @@ def get_start_and_end_times(video_length, length_of_clip):
def download_background(): def download_background():
"""Downloads the backgrounds/s video from youtube.""" """Downloads the backgrounds/s video from YouTube."""
Path("./assets/backgrounds/").mkdir(parents=True, exist_ok=True) Path("./assets/backgrounds/").mkdir(parents=True, exist_ok=True)
background_options = [ # uri , filename , credit background_options = [ # uri , filename , credit
("https://www.youtube.com/watch?v=n_Dv4JMiwK8", "parkour.mp4", "bbswitzer"), ("https://www.youtube.com/watch?v=n_Dv4JMiwK8", "parkour.mp4", "bbswitzer"),
( # (
"https://www.youtube.com/watch?v=2X9QGY__0II", # "https://www.youtube.com/watch?v=2X9QGY__0II",
"rocket_league.mp4", # "rocket_league.mp4",
"Orbital Gameplay", # "Orbital Gameplay",
), # ),
] ]
# note: make sure the file name doesn't include a - in it # note: make sure the file name doesn't include an - in it
if len(listdir("./assets/backgrounds")) != len( if not len(listdir("./assets/backgrounds")) >= len(
background_options background_options
): # if there are any background videos not installed ): # if there are any background videos not installed
print_step( print_step(
@ -35,14 +33,14 @@ def download_background():
) )
print_substep("Downloading the backgrounds videos... please be patient 🙏 ") print_substep("Downloading the backgrounds videos... please be patient 🙏 ")
for uri, filename, credit in background_options: for uri, filename, credit in background_options:
if Path(f"assets/backgrounds/{credit}-{filename}").is_file():
continue # adds check to see if file exists before downloading
print_substep(f"Downloading {filename} from {uri}") print_substep(f"Downloading {filename} from {uri}")
YouTube(uri).streams.filter(res="1080p").first().download( YouTube(uri).streams.filter(res="1080p").first().download(
"assets/backgrounds", filename=f"{credit}-{filename}" "assets/backgrounds", filename=f"{credit}-{filename}"
) )
print_substep( print_substep("Background videos downloaded successfully! 🎉", style="bold green")
"Background videos downloaded successfully! 🎉", style="bold green"
)
def chop_background_video(video_length): def chop_background_video(video_length):
@ -60,5 +58,4 @@ def chop_background_video(video_length):
targetname="assets/temp/background.mp4", targetname="assets/temp/background.mp4",
) )
print_substep("Background video chopped successfully!", style="bold green") print_substep("Background video chopped successfully!", style="bold green")
noerror = True return True
return noerror

@ -1,8 +1,8 @@
[ [
{ {
"name": "USER", "name": "USER",
"value": "eyJwcmVmcyI6eyJ0b3BDb250ZW50RGlzbWlzc2FsVGltZSI6MCwiZ2xvYmFsVGhlbWUiOiJSRURESVQiLCJuaWdodG1vZGUiOnRydWUsImNvbGxhcHNlZFRyYXlTZWN0aW9ucyI6eyJmYXZvcml0ZXMiOmZhbHNlLCJtdWx0aXMiOmZhbHNlLCJtb2RlcmF0aW5nIjpmYWxzZSwic3Vic2NyaXB0aW9ucyI6ZmFsc2UsInByb2ZpbGVzIjpmYWxzZX0sInRvcENvbnRlbnRUaW1lc0Rpc21pc3NlZCI6MH19", "value": "eyJwcmVmcyI6eyJ0b3BDb250ZW50RGlzbWlzc2FsVGltZSI6MCwiZ2xvYmFsVGhlbWUiOiJSRURESVQiLCJuaWdodG1vZGUiOnRydWUsImNvbGxhcHNlZFRyYXlTZWN0aW9ucyI6eyJmYXZvcml0ZXMiOmZhbHNlLCJtdWx0aXMiOmZhbHNlLCJtb2RlcmF0aW5nIjpmYWxzZSwic3Vic2NyaXB0aW9ucyI6ZmFsc2UsInByb2ZpbGVzIjpmYWxzZX0sInRvcENvbnRlbnRUaW1lc0Rpc21pc3NlZCI6MH19",
"domain": ".reddit.com", "domain": ".reddit.com",
"path": "/" "path": "/"
} }
] ]

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

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

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

@ -13,17 +13,12 @@ from moviepy.editor import (
CompositeAudioClip, CompositeAudioClip,
CompositeVideoClip, CompositeVideoClip,
) )
import reddit.subreddit
import re
from utils.console import print_step, print_substep
from dotenv import load_dotenv
import os
from moviepy.video.io import ffmpeg_tools from moviepy.video.io import ffmpeg_tools
from rich.console import Console
from reddit import subreddit from reddit import subreddit
from utils.cleanup import cleanup from utils.cleanup import cleanup
from utils.console import print_step, print_substep from utils.console import print_step, print_substep
from rich.console import Console
console = Console() console = Console()
@ -46,7 +41,7 @@ def make_final_video(number_of_clips, length):
audio_clips = [] audio_clips = []
for i in range(0, number_of_clips): for i in range(0, number_of_clips):
audio_clips.append(AudioFileClip(f"assets/temp/mp3/{i}.mp3")) audio_clips.append(AudioFileClip(f"assets/temp/mp3/{i}.mp3"))
audio_clips.insert(0, AudioFileClip(f"assets/temp/mp3/title.mp3")) audio_clips.insert(0, AudioFileClip("assets/temp/mp3/title.mp3"))
audio_concat = concatenate_audioclips(audio_clips) audio_concat = concatenate_audioclips(audio_clips)
audio_composite = CompositeAudioClip([audio_concat]) audio_composite = CompositeAudioClip([audio_concat])
@ -57,12 +52,29 @@ def make_final_video(number_of_clips, length):
# Output Length # Output Length
console.log(f"[bold green] Video Will Be: {int_total_length} Seconds Long") console.log(f"[bold green] Video Will Be: {int_total_length} Seconds Long")
# Gather all images # add title to video
image_clips = [] image_clips = []
# Gather all images
if opacity is None or float(opacity) >= 1: # opacity not set or is set to one OR MORE
image_clips.insert(
0,
ImageClip("assets/temp/png/title.png")
.set_duration(audio_clips[0].duration)
.set_position("center")
.resize(width=W - 100),
)
else:
image_clips.insert(
0,
ImageClip("assets/temp/png/title.png")
.set_duration(audio_clips[0].duration)
.set_position("center")
.resize(width=W - 100)
.set_opacity(float(opacity)),
)
for i in range(0, number_of_clips): for i in range(0, number_of_clips):
if ( if opacity is None or float(opacity) >= 1: # opacity not set or is set to one OR MORE
opacity is None or float(opacity) >= 1
): # opacity not set or is set to one OR MORE
image_clips.append( image_clips.append(
ImageClip(f"assets/temp/png/comment_{i}.png") ImageClip(f"assets/temp/png/comment_{i}.png")
.set_duration(audio_clips[i + 1].duration) .set_duration(audio_clips[i + 1].duration)
@ -77,38 +89,18 @@ def make_final_video(number_of_clips, length):
.resize(width=W - 100) .resize(width=W - 100)
.set_opacity(float(opacity)), .set_opacity(float(opacity)),
) )
if (
opacity is None or float(opacity) >= 1 # if os.path.exists("assets/mp3/posttext.mp3"):
): # opacity not set or is set to one OR MORE # image_clips.insert(
image_clips.insert( # 0,
0, # ImageClip("assets/png/title.png")
ImageClip(f"assets/temp/png/title.png") # .set_duration(audio_clips[0].duration + audio_clips[1].duration)
.set_duration(audio_clips[0].duration) # .set_position("center")
.set_position("center") # .resize(width=W - 100)
.resize(width=W - 100) # .set_opacity(float(opacity)),
.set_opacity(float(opacity)), # )
) # else:
if os.path.exists("assets/mp3/posttext.mp3"): image_concat = concatenate_videoclips(image_clips).set_position(("center", "center"))
image_clips.insert(
0,
ImageClip("assets/png/title.png")
.set_duration(audio_clips[0].duration + audio_clips[1].duration)
.set_position("center")
.resize(width=W - 100)
.set_opacity(float(opacity)),
)
else:
image_clips.insert(
0,
ImageClip("assets/temp/png/title.png")
.set_duration(audio_clips[0].duration)
.set_position("center")
.resize(width=W - 100)
.set_opacity(float(opacity)),
)
image_concat = concatenate_videoclips(image_clips).set_position(
("center", "center")
)
image_concat.audio = audio_composite image_concat.audio = audio_composite
final = CompositeVideoClip([background_clip, image_concat]) final = CompositeVideoClip([background_clip, image_concat])
@ -142,9 +134,7 @@ def make_final_video(number_of_clips, length):
print_substep("the results folder didn't exist so I made it") print_substep("the results folder didn't exist so I made it")
os.mkdir("./results") os.mkdir("./results")
final.write_videofile( final.write_videofile("assets/temp/temp.mp4", fps=30, audio_codec="aac", audio_bitrate="192k")
"assets/temp/temp.mp4", fps=30, audio_codec="aac", audio_bitrate="192k"
)
ffmpeg_tools.ffmpeg_extract_subclip( ffmpeg_tools.ffmpeg_extract_subclip(
"assets/temp/temp.mp4", 0, length, targetname=f"results/{filename}" "assets/temp/temp.mp4", 0, length, targetname=f"results/{filename}"
) )
@ -153,7 +143,7 @@ def make_final_video(number_of_clips, length):
print_step("Removing temporary files 🗑") print_step("Removing temporary files 🗑")
cleanups = cleanup() cleanups = cleanup()
print_substep(f"Removed {cleanups} temporary files 🗑") print_substep(f"Removed {cleanups} temporary files 🗑")
print_substep(f"See result in the results folder!") print_substep("See result in the results folder!")
print_step( print_step(
f"Reddit title: {os.getenv('VIDEO_TITLE')} \n Background Credit: {os.getenv('background_credit')}" f"Reddit title: {os.getenv('VIDEO_TITLE')} \n Background Credit: {os.getenv('background_credit')}"

@ -18,8 +18,8 @@ storymode = False
def download_screenshots_of_reddit_posts(reddit_object, screenshot_num): def download_screenshots_of_reddit_posts(reddit_object, screenshot_num):
"""Downloads screenshots of reddit posts as they are seen on the web. """Downloads screenshots of reddit posts as they are seen on the web.
Args: Args:
reddit_object: The Reddit Object you received in askreddit.py reddit_object: The Reddit Object you received in askreddit.py
screenshot_num: The number of screenshots you want to download. screenshot_num: The number of screenshots you want to download.
""" """
print_step("Downloading screenshots of reddit posts...") print_step("Downloading screenshots of reddit posts...")
@ -40,7 +40,7 @@ def download_screenshots_of_reddit_posts(reddit_object, screenshot_num):
context.add_cookies(cookies) # load preference cookies context.add_cookies(cookies) # load preference cookies
# Get the thread screenshot # Get the thread screenshot
page = context.new_page() page = context.new_page()
page.goto(reddit_object["thread_url"]) page.goto(reddit_object["thread_url"], timeout=0)
page.set_viewport_size(ViewportSize(width=1920, height=1080)) page.set_viewport_size(ViewportSize(width=1920, height=1080))
if page.locator('[data-testid="content-gate"]').is_visible(): if page.locator('[data-testid="content-gate"]').is_visible():
# This means the post is NSFW and requires to click the proceed button. # This means the post is NSFW and requires to click the proceed button.
@ -51,9 +51,7 @@ def download_screenshots_of_reddit_posts(reddit_object, screenshot_num):
'[data-click-id="text"] button' '[data-click-id="text"] button'
).click() # Remove "Click to see nsfw" Button in Screenshot ).click() # Remove "Click to see nsfw" Button in Screenshot
page.locator('[data-test-id="post-content"]').screenshot( page.locator('[data-test-id="post-content"]').screenshot(path="assets/temp/png/title.png")
path="assets/temp/png/title.png"
)
if storymode: if storymode:
page.locator('[data-click-id="text"]').screenshot( page.locator('[data-click-id="text"]').screenshot(
path="assets/temp/png/story_content.png" path="assets/temp/png/story_content.png"
@ -70,7 +68,7 @@ def download_screenshots_of_reddit_posts(reddit_object, screenshot_num):
if page.locator('[data-testid="content-gate"]').is_visible(): if page.locator('[data-testid="content-gate"]').is_visible():
page.locator('[data-testid="content-gate"] button').click() page.locator('[data-testid="content-gate"] button').click()
page.goto(f'https://reddit.com{comment["comment_url"]}') page.goto(f'https://reddit.com{comment["comment_url"]}', timeout=0)
page.locator(f"#t1_{comment['comment_id']}").screenshot( page.locator(f"#t1_{comment['comment_id']}").screenshot(
path=f"assets/temp/png/comment_{idx}.png" path=f"assets/temp/png/comment_{idx}.png"
) )

@ -1,20 +1,20 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from gtts import gTTS from os import getenv
from pathlib import Path from pathlib import Path
from os import getenv, name
import sox import sox
from mutagen import MutagenError from mutagen import MutagenError
from mutagen.mp3 import MP3, HeaderNotFoundError from mutagen.mp3 import MP3, HeaderNotFoundError
from rich.progress import track
from rich.console import Console from rich.console import Console
from rich.progress import track
console = Console() from TTS.swapper import TTS
import re
from utils.console import print_step, print_substep from utils.console import print_step, print_substep
from utils.voice import sanitize_text from utils.voice import sanitize_text
from video_creation.TTSwrapper import TTTTSWrapper
console = Console()
VIDEO_LENGTH: int = 40 # secs VIDEO_LENGTH: int = 40 # secs
@ -22,38 +22,38 @@ VIDEO_LENGTH: int = 40 # secs
def save_text_to_mp3(reddit_obj): def save_text_to_mp3(reddit_obj):
"""Saves Text to MP3 files. """Saves Text to MP3 files.
Args: Args:
reddit_obj : The reddit object you received from the reddit API in the askreddit.py file. reddit_obj : The reddit object you received from the reddit API in the askreddit.py file.
""" """
print_step("Saving Text to MP3 files...") print_step("Saving Text to MP3 files...")
length = 0 length = 0
# Create a folder for the mp3 files. # Create a folder for the mp3 files.
Path("assets/temp/mp3").mkdir(parents=True, exist_ok=True) Path("assets/temp/mp3").mkdir(parents=True, exist_ok=True)
TextToSpeech = TTS()
ttttsw = TTTTSWrapper() # tiktok text to speech wrapper TextToSpeech.tts(
ttttsw.tts(
sanitize_text(reddit_obj["thread_title"]), sanitize_text(reddit_obj["thread_title"]),
filename=f"assets/temp/mp3/title.mp3", filename="assets/temp/mp3/title.mp3",
random_speaker=False, random_speaker=False,
) )
try: try:
length += MP3(f"assets/temp/mp3/title.mp3").info.length length += MP3("assets/temp/mp3/title.mp3").info.length
except HeaderNotFoundError: # note to self AudioFileClip except HeaderNotFoundError: # note to self AudioFileClip
length += sox.file_info.duration(f"assets/temp/mp3/title.mp3") length += sox.file_info.duration("assets/temp/mp3/title.mp3")
if getenv("STORYMODE").casefold() == "true": if getenv("STORYMODE").casefold() == "true":
ttttsw.tts( TextToSpeech.tts(
sanitize_text(reddit_obj["thread_content"]), sanitize_text(reddit_obj["thread_content"]),
filename=f"assets/temp/mp3/story_content.mp3", filename="assets/temp/mp3/story_content.mp3",
random_speaker=False, random_speaker=False,
) )
#'story_content' # 'story_content'
com = 0 com = 0
for comment in track((reddit_obj["comments"]), "Saving..."): 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 # ! 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: if length > VIDEO_LENGTH:
break break
ttttsw.tts( TextToSpeech.tts(
sanitize_text(comment["comment_body"]), sanitize_text(comment["comment_body"]),
filename=f"assets/temp/mp3/{com}.mp3", filename=f"assets/temp/mp3/{com}.mp3",
random_speaker=False, random_speaker=False,

Loading…
Cancel
Save