merged develop

pull/403/head
Drugsosos 2 years ago
commit cd24608f86
No known key found for this signature in database
GPG Key ID: 8E35176FE617E28D

@ -1,29 +1,86 @@
REDDIT_CLIENT_ID=""
REDDIT_CLIENT_SECRET=""
REDDIT_USERNAME="" REDDIT_CLIENT_ID="" #fFAGRNJru1FTz70BzhT3Zg
REDDIT_PASSWORD="" #EXPLANATION the ID of your Reddit app of SCRIPT type
#RANGE 12:30
#MATCH_REGEX [-a-zA-Z0-9._~+/]+=*$
#OOB_ERROR The ID should be over 12 and under 30 characters, double check your input.
# Valid options are "yes" and "no" REDDIT_CLIENT_SECRET="" #fFAGRNJru1FTz70BzhT3Zg
REDDIT_2FA="" #EXPLANATION the SECRET of your Reddit app of SCRIPT type
#RANGE 20:40
#MATCH_REGEX [-a-zA-Z0-9._~+/]+=*$
#OOB_ERROR The secret should be over 20 and under 40 characters, double check your input.
#If no, it will ask you a thread link to extract the thread, if yes it will randomize it. REDDIT_USERNAME="" #asdfghjkl
RANDOM_THREAD="yes" #EXPLANATION the username of your reddit account
#RANGE 3:20
#MATCH_REGEX [-_0-9a-zA-Z]+$
#OOB_ERROR A username HAS to be between 3 and 20 characters
# Valid options are "light" and "dark" REDDIT_PASSWORD="" #fFAGRNJru1FTz70BzhT3Zg
THEME="" #EXPLANATION the password of your reddit account
#RANGE 8:None
#OOB_ERROR Password too short
# Enter a subreddit, e.g. "AskReddit" #OPTIONAL
SUBREDDIT="" RANDOM_THREAD="no"
# If set to no, it will ask you a thread link to extract the thread, if yes it will randomize it. Default: "no"
# Filters the comments by range of lenght (min and max characters) REDDIT_2FA="" #no
# Min has to be less or equal to max #MATCH_REGEX ^(yes|no)
# DO NOT INSERT ANY SPACES BETWEEN THE COMMA AND THE VALUES #EXPLANATION Whether you have Reddit 2FA enabled, Valid options are "yes" and "no"
COMMENT_LENGTH_RANGE = "min,max"
# Range is 0 -> 1 SUBREDDIT="AskReddit"
OPACITY="0.9" #EXPLANATION what subreddit to pull posts from, the name of the sub, not the URL
#RANGE 3:20
#MATCH_REGEX [_0-9a-zA-Z]+$
#OOB_ERROR A subreddit name HAS to be between 3 and 20 characters
# The absolute path of the folder where you want to save the final video ALLOW_NSFW="False"
# If empty or wrong, the path will be 'assets/' #EXPLANATION Whether to allow NSFW content, True or False
FINAL_VIDEO_PATH="" #MATCH_REGEX ^(True|False)$
POST_ID=""
#MATCH_REGEX ^((?!://|://).)*$
#EXPLANATION Used if you want to use a specific post. example of one is urdtfx
THEME="LIGHT" #dark
#EXPLANATION sets the Reddit theme, either LIGHT or DARK
#MATCH_REGEX ^(dark|light|DARK|LIGHT)$
TIMES_TO_RUN="" #2
#EXPLANATION used if you want to run multiple times. set to an int e.g. 4 or 29 and leave blank for 1
MAX_COMMENT_LENGTH="500" #500
#EXPLANATION max number of characters a comment can have. default is 500
#RANGE 0:10000
#MATCH_TYPE int
#OOB_ERROR the max comment length should be between 0 and 10000
OPACITY="1" #.8
#EXPLANATION Sets the opacity of the comments when overlayed over the background
#RANGE 0:1
#MATCH_TYPE float
#OOB_ERROR The opacity HAS to be between 0 and 1
# If you want to translate the comments to another language, set the language code here.
# If empty, no translation will be done.
POSTLANG=""
#EXPLANATION Activates the translation feature, set the language code for translate or leave blank
TTSCHOICE="Polly"
#EXPLANATION the backend used for TTS. Without anything specified, the user will be prompted to choose one.
# IMPORTANT NOTE: if you use translate, you need to set this to googletranslate or tiktok and use custom voice in your language
STREAMLABS_VOICE="Joanna"
#EXPLANATION Sets the voice for the Streamlabs Polly TTS Engine. Check the file for more information on different voices.
AWS_VOICE="Joanna"
#EXPLANATION Sets the voice for the AWS Polly TTS Engine. Check the file for more information on different voices.
TIKTOK_VOICE="en_us_006"
#EXPLANATION Sets the voice for the TikTok TTS Engine. Check the file for more information on different voices.
#OPTIONAL
STORYMODE="False"
# IN-PROGRESS - not yet implemented

1
.gitattributes vendored

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

@ -0,0 +1,28 @@
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: bug
assignees: ""
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**System (please complete the following information):**
- Python Version: [e.g. Python 3.6]
- OS: [e.g. Windows 11]
- App version / Branch [e.g. latest, V2.0, master, develop ]
**Additional context**
Add any other context about the problem here.

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered** (optional)
A clear and concise description of any alternative solutions or features you've considered.
**Additional context** (optional)
Add any other context or screenshots about the feature request here.

@ -0,0 +1,25 @@
# Description
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant context. List any dependencies that are required for this change. -->
# Issue Fixes
<!-- Fixes #(issue) if relevant-->
None
# Checklist:
- [ ] I am pushing changes to the **develop** branch
- [ ] I am using the recommended development environment
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have formatted and linted my code using python-black and pylint
- [ ] I have cleaned up unnecessary files
- [ ] My changes generate no new warnings
- [ ] My changes follow the existing code-style
- [ ] My changes are relevant to the project
# Any other information (e.g how to test the changes)
None

@ -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,73 @@
# 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

@ -0,0 +1,40 @@
name: 'Stale issue handler'
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@main
id: stale-issue
name: stale-issue
with:
stale-issue-message: 'This issue is stale because it has been open 7 days with no activity. Remove stale label or comment, or this will be closed in 10 days.'
close-issue-message: 'Issue closed due to being stale. Please reopen if issue persists in latest version.'
days-before-stale: 7
days-before-close: 10
stale-issue-label: 'stale'
close-issue-label: 'outdated'
exempt-issue-labels: 'enhancement,keep,blocked'
exempt-all-issue-milestones: true
operations-per-run: 300
remove-stale-when-updated: true
- uses: actions/stale@main
id: stale-pr
name: stale-pr
with:
stale-pr-message: 'This pull request is stale as it has been open for 7 days with no activity. Remove stale label or comment, or this will be closed in 10 days.'
close-pr-message: 'Pull request closed due to being stale.'
days-before-stale: 7
days-before-close: 10
close-pr-label: 'outdated'
stale-pr-label: 'stale'
exempt-pr-labels: 'keep,blocked,before next release,after next release'
exempt-all-pr-milestones: true
operations-per-run: 300
remove-stale-when-updated: true

88
.gitignore vendored

@ -153,13 +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
results/*
reddit-bot-351418-5560ebc49cac.json
/.idea
*.pyc
video_creation/data/videos.json
video_creation/data/envvars.txt

@ -149,7 +149,7 @@ disable=raw-checker-failed,
suppressed-message, suppressed-message,
useless-suppression, useless-suppression,
deprecated-pragma, deprecated-pragma,
use-symbolic-message-instead use-symbolic-message-instead,
attribute-defined-outside-init, attribute-defined-outside-init,
invalid-name, invalid-name,
missing-docstring, missing-docstring,

@ -0,0 +1,127 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at the [discord server](https://discord.gg/yqNvvDMYpq).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

@ -12,11 +12,17 @@ All types of contributions are encouraged and valued. See the [Table of Contents
## Table of Contents ## Table of Contents
- [Contributing to Reddit Video Maker Bot 🎥](#contributing-to-reddit-video-maker-bot-)
- [Table of Contents](#table-of-contents)
- [I Have a Question](#i-have-a-question) - [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute) - [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs) - [Reporting Bugs](#reporting-bugs)
- [How Do I Submit a Good Bug Report?](#how-do-i-submit-a-good-bug-report)
- [Suggesting Enhancements](#suggesting-enhancements) - [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 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) - [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>
@ -104,7 +111,16 @@ When making your PR, follow these guidelines:
- Your branch has a base of _develop_, **not** _master_ - Your branch has a base of _develop_, **not** _master_
- You are merging your branch into the _develop_ branch - You are merging your branch into the _develop_ branch
- You link any issues that are resolved or fixed by your changes. (this is done by typing "Fixes #\<issue number\>") in your pull request. - You link any issues that are resolved or fixed by your changes. (this is done by typing "Fixes #\<issue number\>") in your pull request
- Where possible, you have used `git pull --rebase`, to avoid creating unnecessary merge commits
- You have meaningful commits, and if possible, follow the commit style guide of `type: explanation`
- Here are the commit types:
- **feat** - a new feature
- **fix** - a bug fix
- **docs** - a change to documentation / commenting
- **style** - formatting changes - does not impact code
- **refactor** - refactored code
- **chore** - updating configs, workflows etc - does not impact code
### Improving The Documentation ### Improving The Documentation

@ -1,49 +1,56 @@
# Reddit Video Maker Bot 🎥 # 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✨. All done WITHOUT video editing or asset compiling. Just pure ✨programming magic✨.
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"> <img src="https://user-images.githubusercontent.com/6053155/170528582-cb6671e7-5a2f-4bd4-a048-0e6cfa54f0f7.png" width="350">
](https://tmrrwinc.ca) </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 🤔
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? 🤔 ... but what if we can automate that process? 🤔
## Disclaimers 🚨 ## 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
- **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. you will then have to upload manually. This is for the sake of avoiding any sort of community guideline issues.
## Requirements ## Requirements
- Python 3.6+ - Python 3.9+
- Playwright (this should install automatically during installation) - Playwright (this should install automatically in installation)
## Installation 👩‍💻 ## Installation 👩‍💻
1. Clone this repository 1. Clone this repository
2. Run `pip3 install -r requirements.txt` 2. Run `pip install -r requirements.txt`
3. Run `python3 -m playwright install` and `python3 -m playwright install-deps`.
4. 3. Run `playwright install` and `playwright install-deps`. (if this fails try adding python -m to the front of the command)
4a **Automatic Install**: Run `python3 main.py` and type 'yes' to activate the setup assistant.
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. 4. Run `python main.py`
required\*\*), visit [the Reddit Apps page.](https://www.reddit.com/prefs/apps) TL;DR set up an app that is a "script".
5. Enjoy 😎
5. Run `python3 main.py` (unless you chose automatic install, then the installer will automatically run main.py) (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)
7. Enjoy 😎
## Video
If you want to see more detailed guide, please refer to the official [documentation](https://luka-hietala.gitbook.io/documentation-for-the-reddit-bot/). https://user-images.githubusercontent.com/66544866/173453972-6526e4e6-c6ef-41c5-ab40-5d275e724e7c.mp4
\*The Documentation is still being developed and worked on, please be patient as we change / add new knowledge!
## Contributing & Ways to improve 📈 ## Contributing & Ways to improve 📈
@ -51,13 +58,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! 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 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 a background that is picked instead of the Minecraft one.
- [x] Allowing users to choose between any subreddit. - [x] Allowing users to choose between any subreddit.
- [ ] Allowing users to change voice. - [x] Allowing users to change voice.
- [ ] Creating better documentation and adding a command line interface. - [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. Please read our [contributing guidelines](CONTRIBUTING.md) for more detailed information.
@ -65,10 +73,12 @@ Please read our [contributing guidelines](CONTRIBUTING.md) for more detailed inf
Elebumm (Lewis#6305) - https://github.com/elebumm (Founder) Elebumm (Lewis#6305) - https://github.com/elebumm (Founder)
CallumIO - https://github.com/CallumIO Jason (JasonLovesDoggo#1904) - https://github.com/JasonLovesDoggo
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,19 @@
#!/usr/bin/env python3
import random
import os
from gtts import gTTS
max_chars = 0
class GTTS:
def __init__(self):
self.max_chars = 0
self.voices = []
def run(self, text, filepath):
tts = gTTS(text=text, lang=os.getenv("POSTLANG") or "en", slow=False)
tts.save(filepath)
def randomvoice(self):
return random.choice(self.voices)

@ -0,0 +1,98 @@
import base64
import os
import random
import requests
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 TikTok: # 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="
)
self.max_chars = 300
self.voices = {"human": human, "nonhuman": nonhuman, "noneng": noneng}
def run(self, text, filepath, random_voice: bool = False):
# if censor:
# req_text = pf.censor(req_text)
# pass
voice = (
self.randomvoice()
if random_voice
else (os.getenv("TIKTOK_VOICE") or random.choice(self.voices["human"]))
)
try:
r = requests.post(f"{self.URI_BASE}{voice}&req_text={text}&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={text}&speaker_map_type=0")
# print(r.text)
vstr = [r.json()["data"]["v_str"]][0]
b64d = base64.b64decode(vstr)
with open(filepath, "wb") as out:
out.write(b64d)
def randomvoice(self):
return random.choice(self.voices["human"])

@ -0,0 +1,66 @@
#!/usr/bin/env python3
from boto3 import Session
from botocore.exceptions import BotoCoreError, ClientError
import sys
import os
import random
voices = [
"Brian",
"Emma",
"Russell",
"Joey",
"Matthew",
"Joanna",
"Kimberly",
"Amy",
"Geraint",
"Nicole",
"Justin",
"Ivy",
"Kendra",
"Salli",
"Raveena",
]
class AWSPolly:
def __init__(self):
self.max_chars = 0
self.voices = voices
def run(self, text, filepath, random_voice: bool = False):
session = Session(profile_name="polly")
polly = session.client("polly")
if random_voice:
voice = self.randomvoice()
else:
if not os.getenv("AWS_VOICE"):
return ValueError(
f"Please set the environment variable AWS_VOICE to a valid voice. options are: {voices}"
)
voice = str(os.getenv("AWS_VOICE")).capitalize()
try:
# Request speech synthesis
response = polly.synthesize_speech(
Text=text, OutputFormat="mp3", VoiceId=voice, Engine="neural"
)
except (BotoCoreError, ClientError) as error:
# The service returned an error, exit gracefully
print(error)
sys.exit(-1)
# Access the audio stream from the response
if "AudioStream" in response:
file = open(filepath, "wb")
file.write(response["AudioStream"].read())
file.close()
# print_substep(f"Saved Text {idx} to MP3 files successfully.", style="bold green")
else:
# The response didn't contain audio data, exit gracefully
print("Could not stream audio")
sys.exit(-1)
def randomvoice(self):
return random.choice(self.voices)

@ -0,0 +1,118 @@
#!/usr/bin/env python3
from pathlib import Path
from typing import Tuple
import re
from os import getenv
# import sox
# from mutagen import MutagenError
# from mutagen.mp3 import MP3, HeaderNotFoundError
import translators as ts
from rich.progress import track
from moviepy.editor import AudioFileClip, CompositeAudioClip, concatenate_audioclips
from utils.console import print_step, print_substep
from utils.voice import sanitize_text
DEFUALT_MAX_LENGTH: int = 50 # video length variable
class TTSEngine:
"""Calls the given TTS engine to reduce code duplication and allow multiple TTS engines.
Args:
tts_module : The TTS module. Your module should handle the TTS itself and saving to the given path under the run method.
reddit_object : The reddit object that contains the posts to read.
path (Optional) : The unix style path to save the mp3 files to. This must not have leading or trailing slashes.
max_length (Optional) : The maximum length of the mp3 files in total.
Notes:
tts_module must take the arguments text and filepath.
"""
def __init__(
self,
tts_module,
reddit_object: dict,
path: str = "assets/temp/mp3",
max_length: int = DEFUALT_MAX_LENGTH,
):
self.tts_module = tts_module()
self.reddit_object = reddit_object
self.path = path
self.max_length = max_length
self.length = 0
def run(self) -> Tuple[int, int]:
Path(self.path).mkdir(parents=True, exist_ok=True)
# This file needs to be removed in case this post does not use post text, so that it wont appear in the final video
try:
Path(f"{self.path}/posttext.mp3").unlink()
except OSError:
pass
print_step("Saving Text to MP3 files...")
self.call_tts("title", self.reddit_object["thread_title"])
if self.reddit_object["thread_post"] != "" and getenv("STORYMODE", "").casefold() == "true":
self.call_tts("posttext", self.reddit_object["thread_post"])
idx = None
for idx, comment in track(enumerate(self.reddit_object["comments"]), "Saving..."):
# ! Stop creating mp3 files if the length is greater than max length.
if self.length > self.max_length:
break
if not self.tts_module.max_chars:
self.call_tts(f"{idx}", comment["comment_body"])
else:
self.split_post(comment["comment_body"], idx)
print_substep("Saved Text to MP3 files successfully.", style="bold green")
return self.length, idx
def split_post(self, text: str, idx: int):
split_files = []
split_text = [
x.group().strip()
for x in re.finditer(rf" *((.{{0,{self.tts_module.max_chars}}})(\.|.$))", text)
]
idy = None
for idy, text_cut in enumerate(split_text):
# print(f"{idx}-{idy}: {text_cut}\n")
self.call_tts(f"{idx}-{idy}.part", text_cut)
split_files.append(AudioFileClip(f"{self.path}/{idx}-{idy}.part.mp3"))
CompositeAudioClip([concatenate_audioclips(split_files)]).write_audiofile(
f"{self.path}/{idx}.mp3", fps=44100, verbose=False, logger=None
)
for i in split_files:
name = i.filename
i.close()
Path(name).unlink()
# for i in range(0, idy + 1):
# print(f"Cleaning up {self.path}/{idx}-{i}.part.mp3")
# Path(f"{self.path}/{idx}-{i}.part.mp3").unlink()
def call_tts(self, filename: str, text: str):
self.tts_module.run(text=process_text(text), filepath=f"{self.path}/{filename}.mp3")
# try:
# self.length += MP3(f"{self.path}/{filename}.mp3").info.length
# except (MutagenError, HeaderNotFoundError):
# self.length += sox.file_info.duration(f"{self.path}/{filename}.mp3")
clip = AudioFileClip(f"{self.path}/{filename}.mp3")
self.length += clip.duration
clip.close()
def process_text(text: str):
lang = getenv("POSTLANG", "")
new_text = sanitize_text(text)
if lang:
print_substep("Translating Text...")
translated_text = ts.google(text, to_language=lang)
new_text = sanitize_text(translated_text)
return new_text

@ -0,0 +1,60 @@
import random
import os
import requests
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 StreamlabsPolly:
def __init__(self):
self.url = "https://streamlabs.com/polly/speak"
self.max_chars = 550
self.voices = voices
def run(self, text, filepath, random_voice: bool = False):
if random_voice:
voice = self.randomvoice()
else:
if not os.getenv("STREAMLABS_VOICE"):
return ValueError(
f"Please set the environment variable STREAMLABS_VOICE to a valid voice. options are: {voices}"
)
voice = str(os.getenv("STREAMLABS_VOICE")).capitalize()
body = {"voice": voice, "text": text, "service": "polly"}
response = requests.post(self.url, data=body)
try:
voice_data = requests.get(response.json()["speak_url"])
with open(filepath, "wb") as f:
f.write(voice_data.content)
except (KeyError, JSONDecodeError):
try:
if response.json()["error"] == "No text specified!":
raise ValueError("Please specify a text to convert to speech.")
except (KeyError, JSONDecodeError):
print("Error occurred calling Streamlabs Polly")
def randomvoice(self):
return random.choice(self.voices)
# StreamlabsPolly().run(text=str('hi hi ' * 92)[1:], filepath='hello.mp3', random_voice=True)

Binary file not shown.

Binary file not shown.

@ -1,100 +1,75 @@
#!/usr/bin/env python3 #!/usr/bin/env python
# Main import math
from utils.console import print_markdown from subprocess import Popen
from rich.console import Console from os import getenv, name
import time from dotenv import load_dotenv
from reddit.subreddit import get_subreddit_threads from reddit.subreddit import get_subreddit_threads
from utils.cleanup import cleanup
from utils.console import print_markdown, print_step
from utils.checker import check_env
# 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.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 video_creation.final_video import make_final_video
from dotenv import load_dotenv from video_creation.screenshot_downloader import download_screenshots_of_reddit_posts
import os from video_creation.voices import save_text_to_mp3
console = Console()
configured = True
REQUIRED_VALUES = [
"REDDIT_CLIENT_ID",
"REDDIT_CLIENT_SECRET",
"REDDIT_USERNAME",
"REDDIT_PASSWORD",
"OPACITY",
]
VERSION = "2.2.2"
print(
"""
"""
)
# 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/)"
) )
print_step(f"You are using V{VERSION} of the bot")
""" def main(POST_ID=None):
cleanup()
reddit_object = get_subreddit_threads(POST_ID)
length, number_of_comments = save_text_to_mp3(reddit_object)
length = math.ceil(length)
download_screenshots_of_reddit_posts(reddit_object, number_of_comments)
download_background()
chop_background_video(length)
make_final_video(number_of_comments, length, reddit_object)
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.
""" def run_many(times):
for x in range(1, times + 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()
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")
if __name__ == "__main__":
if check_env() is not True:
exit()
load_dotenv() load_dotenv()
try:
if getenv("TIMES_TO_RUN") and isinstance(int(getenv("TIMES_TO_RUN")), int):
run_many(int(getenv("TIMES_TO_RUN")))
console.log("[bold green]Checking environment variables...") elif len(getenv("POST_ID", "").split("+")) > 1:
time.sleep(1) for index, post_id in enumerate(getenv("POST_ID", "").split("+")):
index += 1
print_step(
if not os.path.exists(".env"): f'on the {index}{("st" if index == 1 else ("nd" if index == 2 else ("rd" if index == 3 else "th")))} post of {len(getenv("POST_ID", "").split("+"))}'
configured = False
console.log("[red] Your .env file is invalid, or was never created. Standby.")
for val in REQUIRED_VALUES:
# print(os.getenv(val))
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) main(post_id)
console.log( Popen("cls" if name == "nt" else "clear", shell=True).wait()
"[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")
elif setup_ask == "no":
console.print("[red]Exiting...")
time.sleep(0.5)
exit()
else: else:
console.print("[red]I don't understand that. Exiting...") main()
time.sleep(0.5) except KeyboardInterrupt:
exit() print_markdown("## Clearing temp files")
try: cleanup()
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
exit() 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,85 +1,100 @@
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
import random
import praw
import re import re
from os import getenv
import praw
from praw.models import MoreComments
console = Console() from utils.console import print_step, print_substep
from utils.subreddit import get_subreddit_undone
from utils.videos import check_done
def get_subreddit_threads(): def get_subreddit_threads(POST_ID: str):
global submission
""" """
Returns a list of threads from the provided subreddit. Returns a list of threads from the AskReddit subreddit.
""" """
load_dotenv() print_substep("Logging into Reddit.")
if os.getenv("REDDIT_2FA", default="no").casefold() == "yes": content = {}
print( if str(getenv("REDDIT_2FA")).casefold() == "yes":
"\nEnter your two-factor authentication code from your authenticator app.\n" print("\nEnter your two-factor authentication code from your authenticator app.\n")
)
code = input("> ") code = input("> ")
print() print()
pw = os.getenv("REDDIT_PASSWORD") pw = getenv("REDDIT_PASSWORD")
passkey = f"{pw}:{code}" passkey = f"{pw}:{code}"
else: else:
passkey = os.getenv("REDDIT_PASSWORD") passkey = getenv("REDDIT_PASSWORD")
username = getenv("REDDIT_USERNAME")
content = {} if username.casefold().startswith("u/"):
username = username[2:]
reddit = praw.Reddit( reddit = praw.Reddit(
client_id=os.getenv("REDDIT_CLIENT_ID"), client_id=getenv("REDDIT_CLIENT_ID"),
client_secret=os.getenv("REDDIT_CLIENT_SECRET"), client_secret=getenv("REDDIT_CLIENT_SECRET"),
user_agent="Accessing subreddit threads", user_agent="Accessing Reddit threads",
username=os.getenv("REDDIT_USERNAME"), username=username,
password=passkey, 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 # Ask user for subreddit input
if not os.getenv("RANDOM_THREAD") or os.getenv("RANDOM_THREAD") == "no": print_step("Getting subreddit threads...")
print_substep("Insert the full thread link:", style="bold green") if not getenv(
thread_link = input() "SUBREDDIT"
print_step("Getting the inserted thread...") ): # note to user. you can have multiple subreddits via reddit.subreddit("redditdev+learnpython")
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: try:
subreddit = reddit.subreddit( subreddit = reddit.subreddit(
re.sub( re.sub(r"r\/", "", input("What subreddit would you like to pull from? "))
r"r\/", # removes the r/ from the input
"",
input("What subreddit would you like to pull from? "),
)
) )
except ValueError: except ValueError:
subreddit = reddit.subreddit("askreddit") subreddit = reddit.subreddit("askreddit")
print_substep("Subreddit not defined. Using AskReddit.") print_substep("Subreddit not defined. Using AskReddit.")
else:
print_substep(f"Using subreddit: r/{getenv('SUBREDDIT')} from environment variable config")
subreddit_choice = getenv("SUBREDDIT")
if subreddit_choice.casefold().startswith("r/"): # removes the r/ from the input
subreddit_choice = subreddit_choice[2:]
subreddit = reddit.subreddit(
subreddit_choice
) # Allows you to specify in .env. Done for automation purposes.
if POST_ID: # would only be called if there are multiple queued posts
submission = reddit.submission(id=POST_ID)
elif getenv("POST_ID") and len(getenv("POST_ID").split("+")) == 1:
submission = reddit.submission(id=getenv("POST_ID"))
else:
threads = subreddit.hot(limit=25) threads = subreddit.hot(limit=25)
submission = list(threads)[random.randrange(0, 25)] submission = get_subreddit_undone(threads, subreddit)
submission = check_done(submission) # double-checking
if submission is None:
return get_subreddit_threads(POST_ID) # submission already done. rerun
upvotes = submission.score
ratio = submission.upvote_ratio * 100
num_comments = submission.num_comments
print_substep(f"Video will be: {submission.title}") print_substep(f"Video will be: {submission.title} :thumbsup:", style="bold green")
print("Getting video comments...") print_substep(f"Thread has {upvotes} upvotes", style="bold blue")
print_substep(f"Thread has a upvote ratio of {ratio}%", style="bold blue")
print_substep(f"Thread has {num_comments} comments", style="bold blue")
try: content["thread_url"] = f"https://reddit.com{submission.permalink}"
content["thread_url"] = submission.url
content["thread_title"] = submission.title content["thread_title"] = submission.title
content["thread_post"] = submission.selftext content["thread_post"] = submission.selftext
content["thread_id"] = submission.id
content["comments"] = [] content["comments"] = []
for top_level_comment in submission.comments: for top_level_comment in submission.comments:
COMMENT_LENGTH_RANGE = [0, Infinity] if isinstance(top_level_comment, MoreComments):
if os.getenv("COMMENT_LENGTH_RANGE"): continue
COMMENT_LENGTH_RANGE = [int(i) for i in os.getenv("COMMENT_LENGTH_RANGE").split(",")] if top_level_comment.body in ["[removed]", "[deleted]"]:
if COMMENT_LENGTH_RANGE[0] <= len(top_level_comment.body) <= COMMENT_LENGTH_RANGE[1]: 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(getenv("MAX_COMMENT_LENGTH", "500")):
if (
top_level_comment.author is not None
): # if errors occur with this change to if not.
content["comments"].append( content["comments"].append(
{ {
"comment_body": top_level_comment.body, "comment_body": top_level_comment.body,
@ -87,9 +102,5 @@ def get_subreddit_threads():
"comment_id": top_level_comment.id, "comment_id": top_level_comment.id,
} }
) )
print_substep("Received subreddit threads Successfully.", style="bold green")
except AttributeError:
pass
print_substep("Received AskReddit threads successfully.", style="bold green")
return content return content

@ -1,8 +1,12 @@
boto3==1.24.12
botocore==1.27.22
gTTS==2.2.4 gTTS==2.2.4
moviepy==1.0.3 moviepy==1.0.3
mutagen==1.45.1 mutagen==1.45.1
playwright==1.22.0 playwright==1.23.0
praw==7.6.0 praw==7.6.0
python-dotenv==0.20.0 python-dotenv==0.20.0
pytube==12.1.0
requests==2.28.1
rich==12.4.4 rich==12.4.4
yt_dlp==2022.5.18 translators==5.3.1

@ -1,209 +0,0 @@
#!/usr/bin/env python3
# Setup Script for RedditVideoMakerBot
# Imports
import os
import subprocess
import re
from utils.console import print_markdown
from utils.console import print_step
from rich.console import Console
from utils.loader import Loader
console = Console()
def handle_input(
message: str = "",
check_type=False,
match: str = "",
err_message: str = "",
nmin=None,
nmax=None,
oob_error="",
):
match = re.compile(match + "$")
while True:
user_input = input(message + "\n> ").strip()
if re.match(match, user_input) is not None:
if check_type is not False:
try:
user_input = check_type(user_input)
if nmin is not None and user_input < nmin:
console.log("[red]" + oob_error) # Input too low failstate
continue
if nmax is not None and user_input > nmax:
console.log("[red]" + oob_error) # Input too high
continue
break # Successful type conversion and number in bounds
except ValueError:
console.log("[red]" + err_message) # Type conversion failed
continue
if (
nmin is not None and len(user_input) < nmin
): # Check if string is long enough
console.log("[red]" + oob_error)
continue
if (
nmax is not None and len(user_input) > nmax
): # Check if string is not too long
console.log("[red]" + oob_error)
continue
break
console.log("[red]" + err_message)
return user_input
if os.path.isfile(".setup-done-before"):
console.log(
"[red]Setup was already completed! Please make sure you have to run this script again. If that is such, delete the file .setup-done-before"
)
exit()
# These lines ensure the user:
# - knows they are in setup mode
# - knows that they are about to erase any other setup files/data.
print_step("Setup Assistant")
print_markdown(
"### You're in the setup wizard. Ensure you're supposed to be here, then type yes to continue. If you're not sure, type no to quit."
)
# This Input is used to ensure the user is sure they want to continue.
if input("Are you sure you want to continue? > ").strip().casefold() != "yes":
console.print("[red]Exiting...")
exit()
# This code is inaccessible if the prior check fails, and thus an else statement is unnecessary
# Again, let them know they are about to erase all other setup data.
console.print(
"[bold red] This will overwrite your current settings. Are you sure you want to continue? [bold green]yes/no"
)
if input("Are you sure you want to continue? > ").strip().casefold() != "yes":
console.print("[red]Abort mission! Exiting...")
exit()
# This is once again inaccessible if the prior checks fail
# Once they confirm, move on with the script.
console.print("[bold green]Alright! Let's get started!")
print("\n")
console.log("Ensure you have the following ready to enter:")
console.log("[bold green]Reddit Client ID")
console.log("[bold green]Reddit Client Secret")
console.log("[bold green]Reddit Username")
console.log("[bold green]Reddit Password")
console.log("[bold green]Reddit 2FA (yes or no)")
console.log("[bold green]Opacity (range of 0-1, decimals are OK)")
console.log("[bold green]Subreddit (without r/ or /r/)")
console.log("[bold green]Theme (light or dark)")
console.print(
"[green]If you don't have these, please follow the instructions in the README.md file to set them up."
)
console.print(
"[green]If you do have these, type yes to continue. If you dont, go ahead and grab those quickly and come back."
)
print()
if input("Are you sure you have the credentials? > ").strip().casefold() != "yes":
console.print("[red]I don't understand that.")
console.print("[red]Exiting...")
exit()
console.print("[bold green]Alright! Let's get started!")
# Begin the setup process.
console.log("Enter your credentials now.")
client_id = handle_input(
"Client ID > ",
False,
"[-a-zA-Z0-9._~+/]+=*",
"That is somehow not a correct ID, try again.",
12,
30,
"The ID should be over 12 and under 30 characters, double check your input.",
)
client_sec = handle_input(
"Client Secret > ",
False,
"[-a-zA-Z0-9._~+/]+=*",
"That is somehow not a correct secret, try again.",
20,
40,
"The secret should be over 20 and under 40 characters, double check your input.",
)
user = handle_input(
"Username > ",
False,
r"[_0-9a-zA-Z]+",
"That is not a valid user",
3,
20,
"A username HAS to be between 3 and 20 characters",
)
passw = handle_input("Password > ", False, ".*", "", 8, None, "Password too short")
twofactor = handle_input(
"2fa Enabled? (yes/no) > ",
False,
r"(yes)|(no)",
"You need to input either yes or no",
)
opacity = handle_input(
"Opacity? (range of 0-1) > ",
float,
".*",
"You need to input a number between 0 and 1",
0,
1,
"Your number is not between 0 and 1",
)
subreddit = handle_input(
"Subreddit (without r/) > ",
False,
r"[_0-9a-zA-Z]+",
"This subreddit cannot exist, make sure you typed it in correctly and removed the r/ (or /r/).",
3,
20,
"A subreddit name HAS to be between 3 and 20 characters",
)
theme = handle_input(
"Theme? (light or dark) > ",
False,
r"(light)|(dark)",
"You need to input 'light' or 'dark'",
)
loader = Loader("Attempting to save your credentials...", "Done!").start()
# you can also put a while loop here, e.g. while VideoIsBeingMade == True: ...
console.log("Writing to the .env file...")
with open(".env", "w") as f:
f.write(
f"""REDDIT_CLIENT_ID="{client_id}"
REDDIT_CLIENT_SECRET="{client_sec}"
REDDIT_USERNAME="{user}"
REDDIT_PASSWORD="{passw}"
REDDIT_2FA="{twofactor}"
THEME="{theme}"
SUBREDDIT="{subreddit}"
OPACITY={opacity}
"""
)
with open(".setup-done-before", "w") as f:
f.write(
"This file blocks the setup assistant from running again. Delete this file to run setup again."
)
loader.stop()
console.log("[bold green]Setup Complete! Returning...")
# Post-Setup: send message and try to run main.py again.
subprocess.call("python3 main.py", shell=True)

@ -0,0 +1,193 @@
#!/usr/bin/env python
import os
from rich.console import Console
from rich.table import Table
from rich import box
import re
import dotenv
from utils.console import handle_input
console = Console()
def check_env() -> bool:
"""Checks to see what's been put in .env
Returns:
bool: Whether or not everything was put in properly
"""
if not os.path.exists(".env.template"):
console.print("[red]Couldn't find .env.template. Unable to check variables.")
return True
if not os.path.exists(".env"):
console.print("[red]Couldn't find the .env file, creating one now.")
with open(".env", "x", encoding="utf-8") as file:
file.write("")
success = True
with open(".env.template", "r", encoding="utf-8") as template:
# req_envs = [env.split("=")[0] for env in template.readlines() if "=" in env]
matching = {}
explanations = {}
bounds = {}
types = {}
oob_errors = {}
examples = {}
req_envs = []
var_optional = False
for line in template.readlines():
if line.startswith("#") is not True and "=" in line and var_optional is not True:
req_envs.append(line.split("=")[0])
if "#" in line:
examples[line.split("=")[0]] = "#".join(line.split("#")[1:]).strip()
elif "#OPTIONAL" in line:
var_optional = True
elif line.startswith("#MATCH_REGEX "):
matching[req_envs[-1]] = line.removeprefix("#MATCH_REGEX ")[:-1]
var_optional = False
elif line.startswith("#OOB_ERROR "):
oob_errors[req_envs[-1]] = line.removeprefix("#OOB_ERROR ")[:-1]
var_optional = False
elif line.startswith("#RANGE "):
bounds[req_envs[-1]] = tuple(
map(
lambda x: float(x) if x != "None" else None,
line.removeprefix("#RANGE ")[:-1].split(":"),
)
)
var_optional = False
elif line.startswith("#MATCH_TYPE "):
types[req_envs[-1]] = eval(line.removeprefix("#MATCH_TYPE ")[:-1].split()[0])
var_optional = False
elif line.startswith("#EXPLANATION "):
explanations[req_envs[-1]] = line.removeprefix("#EXPLANATION ")[:-1]
var_optional = False
else:
var_optional = False
missing = set()
incorrect = set()
dotenv.load_dotenv()
for env in req_envs:
value = os.getenv(env)
if value is None:
missing.add(env)
continue
if env in matching.keys():
re.match(matching[env], value) is None and incorrect.add(env)
if env in bounds.keys() and env not in types.keys():
len(value) >= bounds[env][0] or (
len(bounds[env]) > 1 and bounds[env][1] >= len(value)
) or incorrect.add(env)
continue
if env in types.keys():
try:
temp = types[env](value)
if env in bounds.keys():
(bounds[env][0] <= temp or incorrect.add(env)) and len(bounds[env]) > 1 and (
bounds[env][1] >= temp or incorrect.add(env)
)
except ValueError:
incorrect.add(env)
if len(missing):
table = Table(
title="Missing variables",
highlight=True,
show_lines=True,
box=box.ROUNDED,
border_style="#414868",
header_style="#C0CAF5 bold",
title_justify="left",
title_style="#C0CAF5 bold",
)
table.add_column("Variable", justify="left", style="#7AA2F7 bold", no_wrap=True)
table.add_column("Explanation", justify="left", style="#BB9AF7", no_wrap=False)
table.add_column("Example", justify="center", style="#F7768E", no_wrap=True)
table.add_column("Min", justify="right", style="#F7768E", no_wrap=True)
table.add_column("Max", justify="left", style="#F7768E", no_wrap=True)
for env in missing:
table.add_row(
env,
explanations[env] if env in explanations.keys() else "No explanation given",
examples[env] if env in examples.keys() else "",
str(bounds[env][0]) if env in bounds.keys() and bounds[env][1] is not None else "",
str(bounds[env][1])
if env in bounds.keys() and len(bounds[env]) > 1 and bounds[env][1] is not None
else "",
)
console.print(table)
success = False
if len(incorrect):
table = Table(
title="Incorrect variables",
highlight=True,
show_lines=True,
box=box.ROUNDED,
border_style="#414868",
header_style="#C0CAF5 bold",
title_justify="left",
title_style="#C0CAF5 bold",
)
table.add_column("Variable", justify="left", style="#7AA2F7 bold", no_wrap=True)
table.add_column("Current value", justify="left", style="#F7768E", no_wrap=False)
table.add_column("Explanation", justify="left", style="#BB9AF7", no_wrap=False)
table.add_column("Example", justify="center", style="#F7768E", no_wrap=True)
table.add_column("Min", justify="right", style="#F7768E", no_wrap=True)
table.add_column("Max", justify="left", style="#F7768E", no_wrap=True)
for env in incorrect:
table.add_row(
env,
os.getenv(env),
explanations[env] if env in explanations.keys() else "No explanation given",
str(types[env].__name__) if env in types.keys() else "str",
str(bounds[env][0]) if env in bounds.keys() else "None",
str(bounds[env][1]) if env in bounds.keys() and len(bounds[env]) > 1 else "None",
)
missing.add(env)
console.print(table)
success = False
if success is True:
return True
console.print(
"[green]Do you want to automatically overwrite incorrect variables and add the missing variables? (y/n)"
)
if not input().casefold().startswith("y"):
console.print("[red]Aborting: Unresolved missing variables")
return False
if len(incorrect):
with open(".env", "r+", encoding="utf-8") as env_file:
lines = []
for line in env_file.readlines():
line.split("=")[0].strip() not in incorrect and lines.append(line)
env_file.seek(0)
env_file.write("\n".join(lines))
env_file.truncate()
console.print("[green]Successfully removed incorrectly set variables from .env")
with open(".env", "a", encoding="utf-8") as env_file:
for env in missing:
env_file.write(
env
+ "="
+ ('"' if env not in types.keys() else "")
+ str(
handle_input(
"[#F7768E bold]" + env + "[#C0CAF5 bold]=",
types[env] if env in types.keys() else False,
matching[env] if env in matching.keys() else ".*",
explanations[env]
if env in explanations.keys()
else "Incorrect input. Try again.",
bounds[env][0] if env in bounds.keys() else None,
bounds[env][1] if env in bounds.keys() and len(bounds[env]) > 1 else None,
oob_errors[env] if env in oob_errors.keys() else "Input too long/short.",
extra_info="[#C0CAF5 bold]⮶ "
+ (explanations[env] if env in explanations.keys() else "No info available"),
)
)
+ ('"' if env not in types.keys() else "")
+ "\n"
)
return True
if __name__ == "__main__":
check_env()

@ -0,0 +1,27 @@
import os
from os.path import exists
def cleanup() -> int:
"""Deletes all temporary assets in assets/temp
Returns:
int: How many files were deleted
"""
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,46 @@
# 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)
# def temp():
# root = ''
# if isinstance(root, praw.models.Submission):
# root_type = 'submission'
# elif isinstance(root, praw.models.Comment):
# root_type = 'comment'
#

@ -4,6 +4,8 @@ from rich.markdown import Markdown
from rich.padding import Padding from rich.padding import Padding
from rich.panel import Panel from rich.panel import Panel
from rich.text import Text from rich.text import Text
from rich.columns import Columns
import re
console = Console() console = Console()
@ -25,3 +27,50 @@ def print_step(text):
def print_substep(text, style=""): def print_substep(text, style=""):
"""Prints a rich info message without the panelling.""" """Prints a rich info message without the panelling."""
console.print(text, style=style) console.print(text, style=style)
def print_table(items):
"""Prints items in a table."""
console.print(Columns([Panel(f"[yellow]{item}", expand=True) for item in items]))
def handle_input(
message: str = "",
check_type=False,
match: str = "",
err_message: str = "",
nmin=None,
nmax=None,
oob_error="",
extra_info="",
):
match = re.compile(match + "$")
console.print(extra_info, no_wrap=True)
while True:
console.print(message, end="")
user_input = input("").strip()
if re.match(match, user_input) is not None:
if check_type is not False:
try:
user_input = check_type(user_input) # this line is fine
if nmin is not None and user_input < nmin:
console.print("[red]" + oob_error) # Input too low failstate
continue
if nmax is not None and user_input > nmax:
console.print("[red]" + oob_error) # Input too high
continue
break # Successful type conversion and number in bounds
except ValueError:
console.print("[red]" + err_message) # Type conversion failed
continue
if nmin is not None and len(user_input) < nmin: # Check if string is long enough
console.print("[red]" + oob_error)
continue
if nmax is not None and len(user_input) > nmax: # Check if string is not too long
console.print("[red]" + oob_error)
continue
break
console.print("[red]" + err_message)
return user_input

@ -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.

@ -0,0 +1,54 @@
import json
from os import getenv
from utils.console import print_substep
def get_subreddit_undone(submissions: list, subreddit):
"""_summary_
Args:
submissions (list): List of posts that are going to potentially be generated into a video
subreddit (praw.Reddit.SubredditHelper): Chosen subreddit
Returns:
Any: The submission that has not been done
"""
# recursively checks if the top submission in the list was already done.
with open("./video_creation/data/videos.json", "r", encoding="utf-8") 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:
try:
if getenv("ALLOW_NSFW").casefold() == "false":
print_substep("NSFW Post Detected. Skipping...")
continue
except AttributeError:
print_substep("NSFW settings not defined. Skipping NSFW post...")
if submission.stickied:
print_substep("This post was pinned by moderators. Skipping...")
continue
return submission
print("all submissions have been done going by top submission order")
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) -> bool:
"""Checks to see if the given submission is in the list of videos
Args:
done_videos (list): Finished videos
submission (Any): The submission
Returns:
Boolean: Whether the video was found in the list
"""
for video in done_videos:
if video["id"] == str(submission):
return True
return False

@ -0,0 +1,60 @@
import json
import os
import time
from os import getenv
from typing import Dict
from praw.models import Submission
from utils.console import print_step
def check_done(
redditobj: Submission,
) -> Submission:
# don't set this to be run anyplace that isn't subreddit.py bc of inspect stack
"""Checks if the chosen post has already been generated
Args:
redditobj (Dict[str]): Reddit object gotten from reddit/subreddit.py
Returns:
Dict[str]|None: Reddit object in args
"""
with open("./video_creation/data/videos.json", "r", encoding="utf-8") 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
def save_data(filename: str, reddit_title: str, reddit_id: str):
"""Saves the videos that have already been generated to a JSON file in video_creation/data/videos.json
Args:
filename (str): The finished video title name
@param filename:
@param reddit_id:
@param reddit_title:
"""
with open("./video_creation/data/videos.json", "r+", encoding="utf-8") as raw_vids:
done_vids = json.load(raw_vids)
if reddit_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": reddit_id,
"time": str(int(time.time())),
"background_credit": str(os.getenv("background_credit")),
"reddit_title": reddit_title,
"filename": filename,
}
done_vids.append(payload)
raw_vids.seek(0)
json.dump(done_vids, raw_vids, ensure_ascii=False, indent=4)

@ -0,0 +1,27 @@
import re
def sanitize_text(text: str) -> str:
r"""Sanitizes the text for tts.
What gets removed:
- following characters`^_~@!&;#:-%“”‘"%*/{}[]()\|<>?=+`
- any http or https links
Args:
text (str): Text to be sanitized
Returns:
str: Sanitized text
"""
# 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)
result = result.replace("+", "plus").replace("&", "and")
# remove extra whitespace
return " ".join(result.split())

@ -1,75 +1,83 @@
#!/usr/bin/env python3 import random
from os import listdir, environ
from pathlib import Path
from random import randrange from random import randrange
from typing import Tuple
from yt_dlp import YoutubeDL
from pathlib import Path
from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
from moviepy.editor import VideoFileClip from moviepy.editor import VideoFileClip
from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
from pytube import YouTube
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: int, length_of_clip: int) -> Tuple[int, int]:
"""Generates a random interval of time to be used as the background of the video.
def get_start_and_end_times(video_length, length_of_clip): Args:
video_length (int): Length of the video
length_of_clip (int): Length of the video to be used as the background
Returns:
tuple[int,int]: Start and end time of the randomized interval
"""
random_time = randrange(180, int(length_of_clip) - int(video_length)) random_time = randrange(180, int(length_of_clip) - int(video_length))
return random_time, random_time + video_length return random_time, random_time + video_length
def download_background(video_length):
"""Downloads the background video from youtube. def download_background():
"""Downloads the backgrounds/s video from YouTube."""
Shoutout to: bbswitzer (https://www.youtube.com/watch?v=n_Dv4JMiwK8) 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"),
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.") # "https://www.youtube.com/watch?v=2X9QGY__0II",
print_substep(f"Make sure the video is longer than {str(datetime.timedelta(seconds=round(video_length + 180)))}!\n", style="red") # "rocket_league.mp4",
# "Orbital Gameplay",
inp = input("URL: ") # ),
]
if not inp: # note: make sure the file name doesn't include an - in it
vidurl = "https://www.youtube.com/watch?v=n_Dv4JMiwK8" if not len(listdir("./assets/backgrounds")) >= len(
else: background_options
vidurl = inp ): # if there are any background videos not installed
vidpath = vidurl.split("v=")[1]
if not Path(f"assets/mp4/{vidpath}.mp4").is_file():
print_step( 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:
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}")
YouTube(uri).streams.filter(res="1080p").first().download(
"assets/backgrounds", filename=f"{credit}-{filename}"
) )
print_substep("Downloading the background video... please be patient.") print_substep("Background videos downloaded successfully! 🎉", style="bold green")
ydl_opts = {
"outtmpl": f"assets/mp4/{vidpath}.mp4",
"merge_output_format": "mp4",
}
with YoutubeDL(ydl_opts) as ydl:
ydl.download(vidurl)
print_substep("Background video downloaded successfully!", style="bold green") def chop_background_video(video_length: int):
"""Generates the background footage to be used in the video and writes it to assets/temp/background.mp4
return vidpath Args:
video_length (int): Length of the clip where the background footage is to be taken out of
"""
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) start_time, end_time = get_start_and_end_times(video_length, background.duration)
try:
ffmpeg_extract_subclip( ffmpeg_extract_subclip(
f"assets/mp4/{vidpath}.mp4", f"assets/backgrounds/{choice}",
start_time, start_time,
end_time, end_time,
targetname="assets/mp4/clip.mp4", targetname="assets/temp/background.mp4",
) )
except (OSError, IOError): # ffmpeg issue see #348
print_substep("FFMPEG issue. Trying again...")
with VideoFileClip(f"assets/backgrounds/{choice}") as video:
new = video.subclip(start_time, end_time)
new.write_videofile("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 noerror

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

@ -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,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import multiprocessing
import os
import re
from os.path import exists
from typing import Dict
from moviepy.editor import ( from moviepy.editor import (
VideoFileClip, VideoFileClip,
AudioFileClip, AudioFileClip,
@ -8,11 +14,14 @@ from moviepy.editor import (
CompositeAudioClip, CompositeAudioClip,
CompositeVideoClip, CompositeVideoClip,
) )
import reddit.subreddit from moviepy.video.io import ffmpeg_tools
import re from rich.console import Console
from utils.cleanup import cleanup
from utils.console import print_step, print_substep from utils.console import print_step, print_substep
from dotenv import load_dotenv from utils.videos import save_data
import os
console = Console()
W, H = 1080, 1920 W, H = 1080, 1920
@ -29,78 +38,97 @@ def name_normalize(
return name return name
def make_final_video(number_of_clips): def make_final_video(number_of_clips: int, length: int, reddit_obj: dict):
"""Gathers audio clips, gathers all screenshots, stitches them together and saves the final video to assets/temp
# Calls opacity from the .env
load_dotenv()
opacity = os.getenv("OPACITY")
print_step("Creating the final video...")
Args:
number_of_clips (int): Index to end at when going through the screenshots
length (int): Length of the video
reddit_obj (dict): The reddit object that contains the posts to read.
"""
print_step("Creating the final video 🎥")
VideoFileClip.reW = lambda clip: clip.resize(width=W) VideoFileClip.reW = lambda clip: clip.resize(width=W)
VideoFileClip.reH = lambda clip: clip.resize(width=H) VideoFileClip.reH = lambda clip: clip.resize(width=H)
opacity = os.getenv("OPACITY")
background_clip = ( background_clip = (
VideoFileClip("assets/mp4/clip.mp4") VideoFileClip("assets/temp/background.mp4")
.without_audio() .without_audio()
.resize(height=H) .resize(height=H)
.crop(x1=1166.6, y1=0, x2=2246.6, y2=1920) .crop(x1=1166.6, y1=0, x2=2246.6, y2=1920)
) )
# Gather all audio clips # Gather all audio clips
audio_clips = [] audio_clips = [AudioFileClip(f"assets/temp/mp3/{i}.mp3") for i in range(number_of_clips)]
for i in range(0, number_of_clips): audio_clips.insert(0, AudioFileClip("assets/temp/mp3/title.mp3"))
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_concat = concatenate_audioclips(audio_clips) audio_concat = concatenate_audioclips(audio_clips)
audio_composite = CompositeAudioClip([audio_concat]) audio_composite = CompositeAudioClip([audio_concat])
# Gather all images console.log(f"[bold green] Video Will Be: {length} Seconds Long")
# add title to video
image_clips = [] image_clips = []
for i in range(0, number_of_clips): # Gather all images
image_clips.append( new_opacity = 1 if opacity is None or float(opacity) >= 1 else float(opacity)
ImageClip(f"assets/png/comment_{i}.png")
.set_duration(audio_clips[i + 1].duration)
.set_position("center")
.resize(width=W - 100)
.set_opacity(float(opacity)),
)
if os.path.exists("assets/mp3/posttext.mp3"):
image_clips.insert( image_clips.insert(
0, 0,
ImageClip("assets/png/title.png") ImageClip("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(new_opacity)
) )
else:
image_clips.insert( for i in range(0, number_of_clips):
0, image_clips.append(
ImageClip("assets/png/title.png") ImageClip(f"assets/temp/png/comment_{i}.png")
.set_duration(audio_clips[0].duration) .set_duration(audio_clips[i + 1].duration)
.set_position("center") .set_position("center")
.resize(width=W - 100) .resize(width=W - 100)
.set_opacity(float(opacity)), .set_opacity(new_opacity)
)
image_concat = concatenate_videoclips(image_clips).set_position(
("center", "center")
) )
# if os.path.exists("assets/mp3/posttext.mp3"):
# 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_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])
final_video_path = "assets/" title = re.sub(r"[^\w\s-]", "", reddit_obj["thread_title"])
if os.getenv("FINAL_VIDEO_PATH"): idx = re.sub(r"[^\w\s-]", "", reddit_obj["thread_id"])
final_video_path = os.getenv("FINAL_VIDEO_PATH") filename = f"{name_normalize(title)}.mp4"
filename = final_video_path + name_normalize(reddit.subreddit.submission.title) + ".mp4" subreddit = os.getenv("SUBREDDIT")
try:
final.write_videofile(filename, fps=30, audio_codec="aac", audio_bitrate="192k") save_data(filename, title, idx)
except:
print_substep("Something's wrong with the path you inserted, the video will be saved in the default path (assets/)", style="bold red") if not exists(f"./results/{subreddit}"):
filename = (re.sub('[?\"%*:|<>]', '', ("assets/" + reddit.subreddit.submission.title + ".mp4"))) print_substep("The results folder didn't exist so I made it")
final.write_videofile(filename, fps=30, audio_codec="aac", audio_bitrate="192k") os.makedirs(f"./results/{subreddit}")
for i in range(0, number_of_clips):
pass final.write_videofile(
"assets/temp/temp.mp4",
fps=30,
audio_codec="aac",
audio_bitrate="192k",
verbose=False,
threads=multiprocessing.cpu_count(),
)
ffmpeg_tools.ffmpeg_extract_subclip(
"assets/temp/temp.mp4", 0, final.duration, targetname=f"results/{subreddit}/{filename}"
)
# os.remove("assets/temp/temp.mp4")
print_step("Removing temporary files 🗑")
cleanups = cleanup()
print_substep(f"Removed {cleanups} temporary files 🗑")
print_substep("See result in the results folder!")
print_step(
f'Reddit title: {reddit_obj["thread_title"]} \n Background Credit: {os.getenv("background_credit")}'
)

@ -1,22 +1,33 @@
#!/usr/bin/env python3 import json
from playwright.sync_api import sync_playwright, ViewportSize import os
from os import getenv
from pathlib import Path from pathlib import Path
from typing import Dict
from playwright.async_api import async_playwright # pylint: disable=unused-import
# do not remove the above line
from playwright.sync_api import sync_playwright, ViewportSize
from rich.progress import track from rich.progress import track
import translators as ts
from utils.console import print_step, print_substep from utils.console import print_step, print_substep
import json
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: dict, screenshot_num: int):
"""Downloads screenshots of reddit posts as seen on the web. Downloads to assets/temp/png
Args: Args:
reddit_object: The Reddit Object you received in askreddit.py reddit_object (Dict[str]): Reddit object received from reddit/subreddit.py
screenshot_num: The number of screenshots you want to download. screenshot_num (int): Number of screenshots to downlaod
""" """
print_step("Downloading screenshots of reddit posts...") print_step("Downloading screenshots of reddit posts...")
# ! Make sure the reddit screenshots folder exists # ! 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: with sync_playwright() as p:
print_substep("Launching Headless Browser...") print_substep("Launching Headless Browser...")
@ -24,33 +35,48 @@ def download_screenshots_of_reddit_posts(reddit_object, screenshot_num, theme):
browser = p.chromium.launch() browser = p.chromium.launch()
context = browser.new_context() context = browser.new_context()
try: if getenv("THEME").upper() == "DARK":
if theme.casefold() == "dark": cookie_file = open("./video_creation/data/cookie-dark-mode.json", encoding="utf-8")
cookie_file = open('video_creation/cookies.json') else:
cookie_file = open("./video_creation/data/cookie-light-mode.json", encoding="utf-8")
cookies = json.load(cookie_file) cookies = json.load(cookie_file)
context.add_cookies(cookies) context.add_cookies(cookies) # load preference cookies
except AttributeError:
pass
# 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.
print_substep("Post is NSFW. You are spicy...") print_substep("Post is NSFW. You are spicy...")
page.locator('[data-testid="content-gate"] button').click() 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( # translate code
path="assets/png/title.png"
if getenv("POSTLANG"):
print_substep("Translating post...")
texts_in_tl = ts.google(reddit_object["thread_title"], to_language=os.getenv("POSTLANG"))
page.evaluate(
"tl_content => document.querySelector('[data-test-id=\"post-content\"] > div:nth-child(3) > div > div').textContent = tl_content",
texts_in_tl,
) )
else:
print_substep("Skipping translation...")
for idx, comment in track( page.locator('[data-test-id="post-content"]').screenshot(path="assets/temp/png/title.png")
enumerate(reddit_object["comments"]), "Downloading screenshots..."
):
if storymode:
page.locator('[data-click-id="text"]').screenshot(
path="assets/temp/png/story_content.png"
)
else:
for idx, comment in enumerate(
track(reddit_object["comments"], "Downloading screenshots...")
):
# Stop if we have reached the screenshot_num # Stop if we have reached the screenshot_num
if idx >= screenshot_num: if idx >= screenshot_num:
break break
@ -58,9 +84,21 @@ def download_screenshots_of_reddit_posts(reddit_object, screenshot_num, theme):
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)
# translate code
if getenv("POSTLANG"):
comment_tl = ts.google(
comment["comment_body"], to_language=os.getenv("POSTLANG")
)
page.evaluate(
'([tl_content, tl_id]) => document.querySelector(`#t1_${tl_id} > div:nth-child(2) > div > div[data-testid="comment"] > div`).textContent = tl_content',
[comment_tl, comment["comment_id"]],
)
page.locator(f"#t1_{comment['comment_id']}").screenshot( page.locator(f"#t1_{comment['comment_id']}").screenshot(
path=f"assets/png/comment_{idx}.png" path=f"assets/temp/png/comment_{idx}.png"
) )
print_substep("Screenshots downloaded Successfully.", style="bold green") print_substep("Screenshots downloaded Successfully.", style="bold green")

@ -1,48 +1,57 @@
#!/usr/bin/env python3 #!/usr/bin/env python
from gtts import gTTS
from pathlib import Path
from mutagen.mp3 import MP3
from utils.console import print_step, print_substep
from rich.progress import track
import re
import os
from typing import Dict, Tuple
def save_text_to_mp3(reddit_obj): from rich.console import Console
"""Saves Text to MP3 files.
Args: from TTS.engine_wrapper import TTSEngine
reddit_obj : The reddit object you received from the reddit API in the askreddit.py file. from TTS.GTTS import GTTS
""" from TTS.streamlabs_polly import StreamlabsPolly
print_step("Saving Text to MP3 files...") from TTS.aws_polly import AWSPolly
length = 0 from TTS.TikTok import TikTok
from utils.console import print_table, print_step
# 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) console = Console()
tts.save("assets/mp3/title.mp3")
length += MP3("assets/mp3/title.mp3").info.length
try: TTSProviders = {
Path("assets/mp3/posttext.mp3").unlink() "GoogleTranslate": GTTS,
except OSError: "AWSPolly": AWSPolly,
pass "StreamlabsPolly": StreamlabsPolly,
"TikTok": TikTok,
}
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"]), "Saving..."): def save_text_to_mp3(reddit_obj) -> Tuple[int, int]:
# ! Stop creating mp3 files if the length is greater than 50 seconds. This can be longer, but this is just a good starting point """Saves text to MP3 files.
if length > 50:
Args:
reddit_obj (dict[str]): Reddit object received from reddit API in reddit/subreddit.py
Returns:
tuple[int,int]: (total length of the audio, the number of comments audio was generated for)
"""
env = os.getenv("TTSCHOICE", "")
if env.casefold() in map(lambda _: _.casefold(), TTSProviders):
text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, env), reddit_obj)
else:
while True:
print_step("Please choose one of the following TTS providers: ")
print_table(TTSProviders)
choice = input("\n")
if choice.casefold() in map(lambda _: _.casefold(), TTSProviders):
break break
comment=comment["comment_body"] print("Unknown Choice")
text=re.sub('((http|https)\:\/\/)?[a-zA-Z0-9\.\/\?\:@\-_=#]+\.([a-zA-Z]){2,6}([a-zA-Z0-9\.\&\/\?\:@\-_=#])*', '', comment) text_to_mp3 = TTSEngine(get_case_insensitive_key_value(TTSProviders, choice), reddit_obj)
tts = gTTS(text, lang="en", slow=False)
tts.save(f"assets/mp3/{idx}.mp3") return text_to_mp3.run()
length += MP3(f"assets/mp3/{idx}.mp3").info.length
print_substep("Saved Text to MP3 files successfully.", style="bold green") def get_case_insensitive_key_value(input_dict, key):
# ! Return the index so we know how many screenshots of comments we need to make. return next(
return length, idx (value for dict_key, value in input_dict.items() if dict_key.lower() == key.lower()),
None,
)

Loading…
Cancel
Save