Merge branch 'develop' into patch-1

pull/1242/head
NoFailer 2 years ago committed by GitHub
commit fa2d048625
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -45,7 +45,7 @@ body:
Python version : [e.g. Python 3.6] Python version : [e.g. Python 3.6]
App version / Branch [e.g. latest, V2.0, master, develop] App version / Branch : [e.g. latest, V2.0, master, develop]
validations: validations:
required: true required: true
- type: checkboxes - type: checkboxes

@ -1,4 +1,4 @@
blank_issues_enabled: true blank_issues_enabled: false
contact_links: contact_links:
- name: Ask a question - name: Ask a question
about: Join our discord server to ask questions and discuss with maintainers and contributors. about: Join our discord server to ask questions and discuss with maintainers and contributors.

1
.gitignore vendored

@ -243,3 +243,4 @@ video_creation/data/videos.json
video_creation/data/envvars.txt video_creation/data/envvars.txt
config.toml config.toml
video_creation/data/videos.json

@ -28,7 +28,7 @@ All types of contributions are encouraged and valued. See the [Table of Contents
## I Have a Question ## I Have a Question
> If you want to ask a question, we assume that you have read the available [Documentation](https://luka-hietala.gitbook.io/documentation-for-the-reddit-bot/). > If you want to ask a question, we assume that you have read the available [Documentation](https://reddit-video-maker-bot.netlify.app/).
Before you ask a question, it is best to search for existing [Issues](https://github.com/elebumm/RedditVideoMakerBot/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. Before you ask a question, it is best to search for existing [Issues](https://github.com/elebumm/RedditVideoMakerBot/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
@ -144,4 +144,4 @@ When making your PR, follow these guidelines:
### Improving The Documentation ### Improving The Documentation
All updates to the documentation should be made in a pull request to [this repo](https://github.com/LukaHietala/reddit-bot-docs) All updates to the documentation should be made in a pull request to [this repo](https://github.com/LukaHietala/RedditVideoMakerBot-website)

136
GUI.py

@ -1,32 +1,118 @@
# Import the server module
import http.server
import webbrowser import webbrowser
from pathlib import Path
# Used "tomlkit" instead of "toml" because it doesn't change formatting on "dump"
import tomlkit
from flask import (
Flask,
redirect,
render_template,
request,
send_from_directory,
url_for,
)
import utils.gui_utils as gui
# Set the hostname # Set the hostname
HOST = "localhost" HOST = "localhost"
# Set the port number # Set the port number
PORT = 4000 PORT = 4000
# Define class to display the index page of the web server # Configure application
class PythonServer(http.server.SimpleHTTPRequestHandler): app = Flask(__name__, template_folder="GUI")
def do_GET(self):
if self.path == "/GUI": # Configure secret key only to use 'flash'
self.path = "index.html" app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
return http.server.SimpleHTTPRequestHandler.do_GET(self)
# Ensure responses aren't cached
# Declare object of the class @app.after_request
webServer = http.server.HTTPServer((HOST, PORT), PythonServer) def after_request(response):
# Print the URL of the webserver, new =2 opens in a new tab response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
print(f"Server started at http://{HOST}:{PORT}/GUI/") response.headers["Expires"] = 0
webbrowser.open(f"http://{HOST}:{PORT}/GUI/", new=2) response.headers["Pragma"] = "no-cache"
print("Website opened in new tab") return response
print("Press Ctrl+C to quit")
try:
# Run the web server # Display index.html
webServer.serve_forever() @app.route("/")
except KeyboardInterrupt: def index():
# Stop the web server return render_template("index.html", file="videos.json")
webServer.server_close()
print("The server is stopped.")
exit() @app.route("/backgrounds", methods=["GET"])
def backgrounds():
return render_template("backgrounds.html", file="backgrounds.json")
@app.route("/background/add", methods=["POST"])
def background_add():
# Get form values
youtube_uri = request.form.get("youtube_uri").strip()
filename = request.form.get("filename").strip()
citation = request.form.get("citation").strip()
position = request.form.get("position").strip()
gui.add_background(youtube_uri, filename, citation, position)
return redirect(url_for("backgrounds"))
@app.route("/background/delete", methods=["POST"])
def background_delete():
key = request.form.get("background-key")
gui.delete_background(key)
return redirect(url_for("backgrounds"))
@app.route("/settings", methods=["GET", "POST"])
def settings():
config_load = tomlkit.loads(Path("config.toml").read_text())
config = gui.get_config(config_load)
# Get checks for all values
checks = gui.get_checks()
if request.method == "POST":
# Get data from form as dict
data = request.form.to_dict()
# Change settings
config = gui.modify_settings(data, config_load, checks)
return render_template(
"settings.html", file="config.toml", data=config, checks=checks
)
# Make videos.json accessible
@app.route("/videos.json")
def videos_json():
return send_from_directory("video_creation/data", "videos.json")
# Make backgrounds.json accessible
@app.route("/backgrounds.json")
def backgrounds_json():
return send_from_directory("utils", "backgrounds.json")
# Make videos in results folder accessible
@app.route("/results/<path:name>")
def results(name):
return send_from_directory("results", name, as_attachment=True)
# Make voices samples in voices folder accessible
@app.route("/voices/<path:name>")
def voices(name):
return send_from_directory("GUI/voices", name, as_attachment=True)
# Run browser and start the app
if __name__ == "__main__":
webbrowser.open(f"http://{HOST}:{PORT}", new=2)
print("Website opened in new tab. Refresh if it didn't load.")
app.run(port=PORT)

@ -0,0 +1,263 @@
{% extends "layout.html" %}
{% block main %}
<!-- Delete Background Modal -->
<div class="modal fade" id="deleteBtnModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete background</h5>
</div>
<div class="modal-body">
Are you sure you want to delete this background?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<form action="background/delete" method="post">
<input type="hidden" id="background-key" name="background-key" value="">
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</div>
</div>
</div>
</div>
<!-- Add Background Modal -->
<div class="modal fade" id="backgroundAddModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add background video</h5>
</div>
<div class="modal-body">
<!-- Add video form -->
<form id="addBgForm" action="background/add" method="post" novalidate>
<div class="form-group row">
<label class="col-4 col-form-label" for="youtube_uri">YouTube URI</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-youtube"></i>
</div>
<input name="youtube_uri" placeholder="https://www.youtube.com/watch?v=..." type="text"
class="form-control">
</div>
<span id="feedbackYT" class="form-text feedback-invalid"></span>
</div>
</div>
<div class="form-group row">
<label for="filename" class="col-4 col-form-label">Filename</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-file-earmark"></i>
</div>
<input name="filename" placeholder="Example: cool-background" type="text"
class="form-control">
</div>
<span id="feedbackFilename" class="form-text feedback-invalid"></span>
</div>
</div>
<div class="form-group row">
<label for="citation" class="col-4 col-form-label">Credits (owner of the video)</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-person-circle"></i>
</div>
<input name="citation" placeholder="YouTube Channel" type="text" class="form-control">
</div>
<span class="form-text text-muted">Include the channel name of the
owner of the background video you are adding.</span>
</div>
</div>
<div class="form-group row">
<label for="position" class="col-4 col-form-label">Position of screenshots</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-arrows-fullscreen"></i>
</div>
<input name="position" placeholder="Example: center" type="text" class="form-control">
</div>
<span class="form-text text-muted">Advanced option (you can leave it
empty). Valid options are "center" and decimal numbers</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button name="submit" type="submit" class="btn btn-success">Add background</button>
</form>
</div>
</div>
</div>
</div>
<main>
<div class="album py-2 bg-light">
<div class="container">
<div class="row justify-content-between mt-2">
<div class="col-12 col-md-3 mb-3">
<input type="text" class="form-control searchFilter" placeholder="Search backgrounds"
onkeyup="searchFilter()">
</div>
<div class="col-12 col-md-2 mb-3">
<button type="button" class="btn btn-primary form-control" data-toggle="modal"
data-target="#backgroundAddModal">
Add background video
</button>
</div>
</div>
<div class="grid row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3" id="backgrounds">
</div>
</div>
</div>
</main>
<script>
var keys = [];
var youtube_urls = [];
// Show background videos
$(document).ready(function () {
$.getJSON("backgrounds.json",
function (data) {
delete data["__comment"];
var background = '';
$.each(data, function (key, value) {
// Add YT urls and keys (for validation)
keys.push(key);
youtube_urls.push(value[0]);
background += '<div class="col">';
background += '<div class="card shadow-sm">';
background += '<iframe class="bd-placeholder-img card-img-top" width="100%" height="225" src="https://www.youtube-nocookie.com/embed/' + value[0].split("?v=")[1] + '" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>';
background += '<div class="card-body">';
background += '<p class="card-text">' + value[2] + ' • ' + key + '</p>';
background += '<div class="d-flex justify-content-between align-items-center">';
background += '<div class="btn-group">';
background += '<button type="button" class="btn btn-outline-danger" data-toggle="modal" data-target="#deleteBtnModal" data-background-key="' + key + '">Delete</button>';
background += '</div>';
background += '</div>';
background += '</div>';
background += '</div>';
background += '</div>';
});
$('#backgrounds').append(background);
});
});
// Add background key when deleting
$('#deleteBtnModal').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget);
var key = button.data('background-key');
$('#background-key').prop('value', key);
});
var searchFilter = () => {
const input = document.querySelector(".searchFilter");
const cards = document.getElementsByClassName("col");
console.log(cards[1])
let filter = input.value
for (let i = 0; i < cards.length; i++) {
let title = cards[i].querySelector(".card-text");
if (title.innerText.toLowerCase().indexOf(filter.toLowerCase()) > -1) {
cards[i].classList.remove("d-none")
} else {
cards[i].classList.add("d-none")
}
}
}
// Validate form
$("#addBgForm").submit(function (event) {
$("#addBgForm input").each(function () {
if (!(validate($(this)))) {
event.preventDefault();
event.stopPropagation();
}
});
});
$('#addBgForm input[type="text"]').on("keyup", function () {
validate($(this));
});
function validate(object) {
let bool = check(object.prop("name"), object.prop("value"));
// Change class
if (bool) {
object.removeClass("is-invalid");
object.addClass("is-valid");
}
else {
object.removeClass("is-valid");
object.addClass("is-invalid");
}
return bool;
// Check values (return true/false)
function check(name, value) {
if (name == "youtube_uri") {
// URI validation
let regex = /(?:\/|%3D|v=|vi=)([0-9A-z-_]{11})(?:[%#?&]|$)/;
if (!(regex.test(value))) {
$("#feedbackYT").html("Invalid URI");
$("#feedbackYT").show();
return false;
}
// Check if this background already exists
if (youtube_urls.includes(value)) {
$("#feedbackYT").html("This background is already added");
$("#feedbackYT").show();
return false;
}
$("#feedbackYT").hide();
return true;
}
if (name == "filename") {
// Check if key is already taken
if (keys.includes(value)) {
$("#feedbackFilename").html("This filename is already taken");
$("#feedbackFilename").show();
return false;
}
let regex = /^([a-zA-Z0-9\s_-]{1,100})$/;
if (!(regex.test(value))) {
return false;
}
return true;
}
if (name == "citation") {
if (value.trim()) {
return true;
}
}
if (name == "position") {
if (!(value == "center" || value.length == 0 || value % 1 == 0)) {
return false;
}
return true;
}
}
}
</script>
{% endblock %}

@ -1,87 +1,5 @@
<html lang="en"> {% extends "layout.html" %}
<head> {% block main %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="cache-control" content="no-cache"/>
<title>RedditVideoMakerBot</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<link href="https://getbootstrap.com/docs/5.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
.b-example-divider {
height: 3rem;
background-color: rgba(0, 0, 0, .1);
border: solid rgba(0, 0, 0, .15);
border-width: 1px 0;
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
}
.b-example-vr {
flex-shrink: 0;
width: 1.5rem;
height: 100vh;
}
.bi {
vertical-align: -.125em;
fill: currentColor;
}
.nav-scroller {
position: relative;
z-index: 2;
height: 2.75rem;
overflow-y: hidden;
}
.nav-scroller .nav {
display: flex;
flex-wrap: nowrap;
padding-bottom: 1rem;
margin-top: -1px;
overflow-x: auto;
text-align: center;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
#tooltip {
background-color: #333;
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 13px;
}
</style>
</head>
<body>
<header>
<div class="navbar navbar-dark bg-dark shadow-sm">
<div class="container">
<a href="#" class="navbar-brand d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="me-2" viewBox="0 0 24 24"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
<strong>RedditVideoMakerBot</strong>
</a>
</div>
</div>
</header>
<main> <main>
<div class="album py-2 bg-light"> <div class="album py-2 bg-light">
@ -89,7 +7,8 @@
<div class="row mt-2"> <div class="row mt-2">
<div class="col-12 col-md-3 mb-3"> <div class="col-12 col-md-3 mb-3">
<input type="text" class="form-control" placeholder="Search videos" aria-label="Search videos" onkeyup="searchFilter()"> <input type="text" class="form-control searchFilter" placeholder="Search videos"
aria-label="Search videos" onkeyup="searchFilter()">
</div> </div>
</div> </div>
@ -100,22 +19,6 @@
</div> </div>
</main> </main>
<footer class="text-muted py-5">
<div class="container">
<p class="float-end mb-1">
<a href="#">Back to top</a>
</p>
<p class="mb-1"><a href="https://getbootstrap.com/docs/5.2/examples/album/" target="_blank">Album</a> Example Theme by &copy; Bootstrap. <a href="https://github.com/elebumm/RedditVideoMakerBot/blob/master/README.md#developers-and-maintainers" target="_blank">Developers and Maintainers</a></p>
<p class="mb-0">If your data is not refreshing, try to hard reload(Ctrl + F5) and visit your local <a href="../video_creation/data/videos.json" target="_blank">videos.json</a> file.</p>
</div>
</footer>
<script src="https://code.jquery.com/jquery-3.1.1.js" integrity="sha256-16cdPddA6VdVInumRGo6IbivbERE8p7CQR3HzTBuELA=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.14.3/dist/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.10/clipboard.min.js"></script>
<script src="https://unpkg.com/isotope-layout@3/dist/isotope.pkgd.js"></script>
<script> <script>
const intervals = [ const intervals = [
{ label: 'year', seconds: 31536000 }, { label: 'year', seconds: 31536000 },
@ -134,12 +37,11 @@
} }
$(document).ready(function () { $(document).ready(function () {
$.getJSON("../video_creation/data/videos.json", $.getJSON("videos.json",
function (data) { function (data) {
data.sort((b, a) => a['time'] - b['time']) data.sort((b, a) => a['time'] - b['time'])
var video = ''; var video = '';
$.each(data, function (key, value) { $.each(data, function (key, value) {
video += '<div class="col">'; video += '<div class="col">';
video += '<div class="card shadow-sm">'; video += '<div class="card shadow-sm">';
//keeping original themed image card for future thumbnail usage video += '<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"/><text x="50%" y="50%" fill="#eceeef" dy=".3em">r/'+value.subreddit+'</text></svg>'; //keeping original themed image card for future thumbnail usage video += '<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"/><text x="50%" y="50%" fill="#eceeef" dy=".3em">r/'+value.subreddit+'</text></svg>';
@ -263,7 +165,7 @@
} }
var searchFilter = () => { var searchFilter = () => {
const input = document.querySelector(".form-control"); const input = document.querySelector(".searchFilter");
const cards = document.getElementsByClassName("col"); const cards = document.getElementsByClassName("col");
console.log(cards[1]) console.log(cards[1])
let filter = input.value let filter = input.value
@ -277,5 +179,4 @@
} }
} }
</script> </script>
</body> {% endblock %}
</html>

@ -0,0 +1,142 @@
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="cache-control" content="no-cache" />
<title>RedditVideoMakerBot</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<link href="https://getbootstrap.com/docs/5.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.feedback-invalid {
color: #dc3545;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
.bi {
vertical-align: -.125em;
fill: currentColor;
}
.nav {
display: flex;
flex-wrap: nowrap;
padding-bottom: 1rem;
margin-top: -1px;
overflow-x: auto;
text-align: center;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
#tooltip {
background-color: #333;
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 13px;
}
.tooltip-inner {
max-width: 500px !important;
}
</style>
</head>
<script src="https://code.jquery.com/jquery-3.1.1.js" integrity="sha256-16cdPddA6VdVInumRGo6IbivbERE8p7CQR3HzTBuELA="
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.14.3/dist/umd/popper.min.js"
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.10/clipboard.min.js"></script>
<script src="https://unpkg.com/isotope-layout@3/dist/isotope.pkgd.js"></script>
<body>
<header>
{% if get_flashed_messages() %}
{% for category, message in get_flashed_messages(with_categories=true) %}
{% if category == "error" %}
<div class="alert alert-danger mb-0 text-center" role="alert">
{{ message }}
</div>
{% else %}
<div class="alert alert-success mb-0 text-center" role="alert">
{{ message }}
</div>
{% endif %}
{% endfor %}
{% endif %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a href="/" class="navbar-brand d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" stroke="currentColor"
stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="me-2"
viewBox="0 0 24 24">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
<circle cx="12" cy="13" r="4" />
</svg>
<strong>RedditVideoMakerBot</strong>
</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="backgrounds">Background Manager</a>
</li>
<li class="nav-item">
<a class="nav-link" href="settings">Settings</a>
</li>
</ul>
<!-- Future feature
<ul class="navbar-nav">
<li class="nav-item">
<button class="btn btn-outline-success mr-auto mt-2 mt-lg-0">Create new short</button>
</li>
</ul>
-->
</div>
</div>
</nav>
</header>
{% block main %}{% endblock %}
<footer class="text-muted py-5">
<div class="container">
<p class="float-end mb-1">
<a href="#">Back to top</a>
</p>
<p class="mb-1"><a href="https://getbootstrap.com/docs/5.2/examples/album/" target="_blank">Album</a>
Example
Theme by &copy; Bootstrap. <a
href="https://github.com/elebumm/RedditVideoMakerBot/blob/master/README.md#developers-and-maintainers"
target="_blank">Developers and Maintainers</a></p>
<p class="mb-0">If your data is not refreshing, try to hard reload(Ctrl + F5) and visit your local
<strong>{{ file }}</strong> file.
</p>
</div>
</footer>
</body>
</html>

@ -0,0 +1,576 @@
{% extends "layout.html" %}
{% block main %}
<main>
<br>
<div class="container">
<form id="settingsForm" action="/settings" method="post" novalidate>
<!-- Reddit Credentials -->
<p class="h4">Reddit Credentials</p>
<div class="row mb-2">
<label for="client_id" class="col-4">Client ID</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-person"></i>
</div>
<input name="client_id" value="{{ data.client_id }}" placeholder="Your Reddit app's client ID"
type="text" class="form-control" data-toggle="tooltip"
data-original-title='Text under "personal use script" on www.reddit.com/prefs/apps'>
</div>
</div>
</div>
<div class="row mb-2">
<label for="client_secret" class="col-4">Client Secret</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-key-fill"></i>
</div>
<input name="client_secret" value="{{ data.client_secret }}"
placeholder="Your Reddit app's client secret" type="text" class="form-control"
data-toggle="tooltip" data-original-title='"Secret" on www.reddit.com/prefs/apps'>
</div>
</div>
</div>
<div class="row mb-2">
<label for="username" class="col-4">Reddit Username</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-person-fill"></i>
</div>
<input name="username" value="{{ data.username }}" placeholder="Your Reddit account's username"
type="text" class="form-control">
</div>
</div>
</div>
<div class="row mb-2">
<label for="password" class="col-4">Reddit Password</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-lock-fill"></i>
</div>
<input name="password" value="{{ data.password }}" placeholder="Your Reddit account's password"
type="password" class="form-control">
</div>
</div>
</div>
<div class="row mb-2">
<label class="col-4">Do you have Reddit 2FA enabled?</label>
<div class="col-8">
<div class="form-check form-switch">
<input name="2fa" class="form-check-input" type="checkbox" value="True" data-toggle="tooltip"
data-original-title='Check it if you have enabled 2FA on your Reddit account'>
</div>
<span class="form-text text-muted"><a
href="https://reddit-video-maker-bot.netlify.app/docs/configuring#setting-up-the-api"
target="_blank">Need help? Click here to open step-by-step guide.</a></span>
</div>
</div>
<!-- Reddit Thread -->
<p class="h4">Reddit Thread</p>
<div class="row mb-2">
<label class="col-4">Random Thread</label>
<div class="col-8">
<div class="form-check form-switch">
<input name="random" class="form-check-input" type="checkbox" value="True" data-toggle="tooltip"
data-original-title='If disabled, it will ask you for a thread link, instead of picking random one'>
</div>
</div>
</div>
<div class="row mb-2">
<label for="subreddit" class="col-4">Subreddit</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-reddit"></i>
</div>
<input value="{{ data.subreddit }}" name="subreddit" type="text" class="form-control"
placeholder="Subreddit to pull posts from (e.g. AskReddit)" data-toggle="tooltip"
data-original-title='You can have multiple subreddits,
add "+" between them (e.g. AskReddit+Redditdev)'>
</div>
</div>
</div>
<div class="row mb-2">
<label for="post_id" class="col-4">Post ID</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-file-text"></i>
</div>
<input value="{{ data.post_id }}" name="post_id" type="text" class="form-control"
placeholder="Used if you want to use a specific post (e.g. urdtfx)">
</div>
</div>
</div>
<div class="row mb-2">
<label for="max_comment_length" class="col-4">Max Comment Length</label>
<div class="col-8">
<div class="input-group">
<input name="max_comment_length" type="range" class="form-range" min="10" max="10000" step="1"
value="{{ data.max_comment_length }}" data-toggle="tooltip"
data-original-title="{{ data.max_comment_length }}">
</div>
<span class="form-text text-muted">Max number of characters a comment can have.</span>
</div>
</div>
<div class="row mb-2">
<label for="post_lang" class="col-4">Post Language</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-translate"></i>
</div>
<input value="{{ data.post_lang }}" name="post_lang" type="text" class="form-control"
placeholder="The language you would like to translate to">
</div>
</div>
</div>
<div class="row mb-2">
<label for="min_comments" class="col-4">Minimum Comments</label>
<div class="col-8">
<div class="input-group">
<input name="min_comments" type="range" class="form-range" min="15" max="1000" step="1"
value="{{ data.min_comments }}" data-toggle="tooltip"
data-original-title="{{ data.min_comments }}">
</div>
<span class="form-text text-muted">The minimum number of comments a post should have to be
included.</span>
</div>
</div>
<!-- General Settings -->
<p class="h4">General Settings</p>
<div class="row mb-2">
<label class="col-4">Allow NSFW</label>
<div class="col-8">
<div class="form-check form-switch">
<input name="allow_nsfw" class="form-check-input" type="checkbox" value="True"
data-toggle="tooltip" data-original-title='If checked NSFW posts will be allowed'>
</div>
</div>
</div>
<div class="row mb-2">
<label for="theme" class="col-4">Reddit Theme</label>
<div class="col-8">
<select name="theme" class="form-select" data-toggle="tooltip"
data-original-title='Sets the theme of Reddit screenshots'>
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
</div>
</div>
<div class="row mb-2">
<label for="times_to_run" class="col-4">Times To Run</label>
<div class="col-8">
<div class="input-group">
<input name="times_to_run" type="range" class="form-range" min="1" max="1000" step="1"
value="{{ data.times_to_run }}" data-toggle="tooltip"
data-original-title="{{ data.times_to_run }}">
</div>
<span class="form-text text-muted">Used if you want to create multiple videos.</span>
</div>
</div>
<div class="row mb-2">
<label for="opacity" class="col-4">Opacity Of Comments</label>
<div class="col-8">
<div class="input-group">
<input name="opacity" type="range" class="form-range" min="0" max="1" step="0.05"
value="{{ data.opacity }}" data-toggle="tooltip" data-original-title="{{ data.opacity }}">
</div>
<span class="form-text text-muted">Sets the opacity of the comments when overlayed over the
background.</span>
</div>
</div>
<div class="row mb-2">
<label for="transition" class="col-4">Transition</label>
<div class="col-8">
<div class="input-group">
<input name="transition" type="range" class="form-range" min="0" max="2" step="0.05"
value="{{ data.transition }}" data-toggle="tooltip"
data-original-title="{{ data.transition }}">
</div>
<span class="form-text text-muted">Sets the transition time (in seconds) between the
comments. Set to 0 if you want to disable it.</span>
</div>
</div>
<div class="row mb-2">
<label for="background_choice" class="col-4">Background Choice</label>
<div class="col-8">
<select name="background_choice" class="form-select" data-toggle="tooltip"
data-original-title='Sets the background of the video'>
<option value=" ">Random Video</option>
{% for background in checks["background_choice"]["options"][1:] %}
<option value="{{background}}">{{background}}</option>
{% endfor %}
</select>
<span class="form-text text-muted"><a href="/backgrounds" target="_blank">See all available
backgrounds</a></span>
</div>
</div>
<!-- TTS Settings -->
<p class="h4">TTS Settings</p>
<div class="row mb-2">
<label for="voice_choice" class="col-4">TTS Voice Choice</label>
<div class="col-8">
<select name="voice_choice" class="form-select" data-toggle="tooltip"
data-original-title='The voice platform used for TTS generation'>
<option value="streamlabspolly">Streamlabspolly</option>
<option value="tiktok">TikTok</option>
<option value="googletranslate">Google Translate</option>
<option value="awspolly">AWS Polly</option>
<option value="pyttsx">Python TTS (pyttsx)</option>
</select>
</div>
</div>
<div class="row mb-2">
<label for="aws_polly_voice" class="col-4">AWS Polly Voice</label>
<div class="col-8">
<div class="input-group voices">
<select name="aws_polly_voice" class="form-select" data-toggle="tooltip"
data-original-title='The voice used for AWS Polly'>
<option value="Brian">Brian</option>
<option value="Emma">Emma</option>
<option value="Russell">Russell</option>
<option value="Joey">Joey</option>
<option value="Matthew">Matthew</option>
<option value="Joanna">Joanna</option>
<option value="Kimberly">Kimberly</option>
<option value="Amy">Amy</option>
<option value="Geraint">Geraint</option>
<option value="Nicole">Nicole</option>
<option value="Justin">Justin</option>
<option value="Ivy">Ivy</option>
<option value="Kendra">Kendra</option>
<option value="Salli">Salli</option>
<option value="Raveena">Raveena</option>
</select>
<button type="button" class="btn btn-primary"><i id="awspolly_icon"
class="bi-volume-up-fill"></i></button>
</div>
</div>
</div>
<div class="row mb-2">
<label for="streamlabs_polly_voice" class="col-4">Streamlabs Polly Voice</label>
<div class="col-8">
<div class="input-group voices">
<select id="streamlabs_polly_voice" name="streamlabs_polly_voice" class="form-select"
data-toggle="tooltip" data-original-title='The voice used for Streamlabs Polly'>
<option value="Brian">Brian</option>
<option value="Emma">Emma</option>
<option value="Russell">Russell</option>
<option value="Joey">Joey</option>
<option value="Matthew">Matthew</option>
<option value="Joanna">Joanna</option>
<option value="Kimberly">Kimberly</option>
<option value="Amy">Amy</option>
<option value="Geraint">Geraint</option>
<option value="Nicole">Nicole</option>
<option value="Justin">Justin</option>
<option value="Ivy">Ivy</option>
<option value="Kendra">Kendra</option>
<option value="Salli">Salli</option>
<option value="Raveena">Raveena</option>
</select>
<button type="button" class="btn btn-primary"><i id="streamlabs_icon"
class="bi bi-volume-up-fill"></i></button>
</div>
</div>
</div>
<div class="row mb-2">
<label for="tiktok_voice" class="col-4">TikTok Voice</label>
<div class="col-8">
<div class="input-group voices">
<select name="tiktok_voice" class="form-select" data-toggle="tooltip"
data-original-title='The voice used for TikTok TTS'>
<option disabled value="">-----Disney Voices-----</option>
<option value="en_us_ghostface">Ghost Face</option>
<option value="en_us_chewbacca">Chewbacca</option>
<option value="en_us_c3po">C3PO</option>
<option value="en_us_stitch">Stitch</option>
<option value="en_us_stormtrooper">Stormtrooper</option>
<option value="en_us_rocket">Rocket</option>
<option disabled value="">-----English Voices-----</option>
<option value="en_au_001">English AU - Female</option>
<option value="en_au_002">English AU - Male</option>
<option value="en_uk_001">English UK - Male 1</option>
<option value="en_uk_003">English UK - Male 2</option>
<option value="en_us_001">English US - Female (Int. 1)</option>
<option value="en_us_002">English US - Female (Int. 2)</option>
<option value="en_us_006">English US - Male 1</option>
<option value="en_us_007">English US - Male 2</option>
<option value="en_us_009">English US - Male 3</option>
<option value="en_us_010">English US - Male 4</option>
<option disabled value="">-----European Voices-----</option>
<option value="fr_001">French - Male 1</option>
<option value="fr_002">French - Male 2</option>
<option value="de_001">German - Female</option>
<option value="de_002">German - Male</option>
<option value="es_002">Spanish - Male</option>
<option disabled value="">-----American Voices-----</option>
<option value="es_mx_002">Spanish MX - Male</option>
<option value="br_001">Portuguese BR - Female 1</option>
<option value="br_003">Portuguese BR - Female 2</option>
<option value="br_004">Portuguese BR - Female 3</option>
<option value="br_005">Portuguese BR - Male</option>
<option disabled value="">-----Asian Voices-----</option>
<option value="id_001">Indonesian - Female</option>
<option value="jp_001">Japanese - Female 1</option>
<option value="jp_003">Japanese - Female 2</option>
<option value="jp_005">Japanese - Female 3</option>
<option value="jp_006">Japanese - Male</option>
<option value="kr_002">Korean - Male 1</option>
<option value="kr_003">Korean - Female</option>
<option value="kr_004">Korean - Male 2</option>
</select>
<button type="button" class="btn btn-primary"><i id="tiktok_icon"
class="bi-volume-up-fill"></i></button>
</div>
</div>
</div>
<div class="row mb-2">
<label for="python_voice" class="col-4">Python Voice</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-mic-fill"></i>
</div>
<input value="{{ data.python_voice }}" name="python_voice" type="text" class="form-control"
data-toggle="tooltip"
data-original-title='The index of the system TTS voices (can be downloaded externally, run ptt.py to find value, start from zero)'>
</div>
</div>
</div>
<div class="row mb-2">
<label for="py_voice_num" class="col-4">Py Voice Number</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-headset"></i>
</div>
<input value="{{ data.py_voice_num }}" name="py_voice_num" type="text" class="form-control"
data-toggle="tooltip"
data-original-title='The number of system voices (2 are pre-installed in Windows)'>
</div>
</div>
</div>
<div class="row mb-2">
<label for="silence_duration" class="col-4">Silence Duration</label>
<div class="col-8">
<div class="input-group">
<input name="silence_duration" type="range" class="form-range" min="0" max="5" step="0.05"
value="{{ data.silence_duration }}" data-toggle="tooltip"
data-original-title="{{ data.silence_duration }}">
</div>
<span class="form-text text-muted">Time in seconds between TTS comments.</span>
</div>
</div>
<div class="col text-center">
<br>
<button id="defaultSettingsBtn" type="button" class="btn btn-secondary">Default
Settings</button>
<button id="submitButton" type="submit" class="btn btn-success">Save
Changes</button>
</div>
</form>
</div>
<audio src=""></audio>
</main>
<script>
// Test voices buttons
var playing = false;
$(".voices button").click(function () {
var icon = $(this).find("i");
var audio = $("audio");
if (playing) {
playing.toggleClass("bi-volume-up-fill bi-stop-fill");
// Clicked the same button - stop audio
if (playing.prop("id") == icon.prop("id")) {
audio[0].pause();
playing = false;
return;
}
}
icon.toggleClass("bi-volume-up-fill bi-stop-fill");
let path = "voices/" + $(this).closest(".voices").find("select").prop("value").toLowerCase() + ".mp3";
audio.prop("src", path);
audio[0].play();
playing = icon;
audio[0].onended = function () {
icon.toggleClass("bi-volume-up-fill bi-stop-fill");
playing = false;
}
});
// Wait for DOM to load
$(document).ready(function () {
// Add tooltips
$('[data-toggle="tooltip"]').tooltip();
$('[data-toggle="tooltip"]').on('click', function () {
$(this).tooltip('hide');
});
// Update slider tooltip
$(".form-range").on("input", function () {
$(this).attr("value", $(this).val());
$(this).attr("data-original-title", $(this).val());
$(this).tooltip("show");
});
// Get current config
var data = JSON.parse('{{data | tojson}}');
// Set current checkboxes
$('.form-check-input').each(function () {
$(this).prop("checked", data[$(this).prop("name")]);
});
// Set current select options
$('.form-select').each(function () {
$(this).prop("value", data[$(this).prop("name")]);
});
// Submit "False" when checkbox isn't ticked
$('#settingsForm').submit(function () {
$('.form-check-input').each(function () {
if (!($(this).is(':checked'))) {
$(this).prop("value", "False");
$(this).prop("checked", true);
}
});
});
// Get validation values
let validateChecks = JSON.parse('{{checks | tojson}}');
// Set default values
$("#defaultSettingsBtn").click(function (event) {
$("#settingsForm input, #settingsForm select").each(function () {
let check = validateChecks[$(this).prop("name")];
if (check["default"]) {
$(this).prop("value", check["default"]);
// Update tooltip value for input[type="range"]
if ($(this).prop("type") == "range") {
$(this).attr("data-original-title", check["default"]);
}
}
});
});
// Validate form
$('#settingsForm').submit(function (event) {
$("#settingsForm input, #settingsForm select").each(function () {
if (!(validate($(this)))) {
event.preventDefault();
event.stopPropagation();
$("html, body").animate({
scrollTop: $(this).offset().top
});
}
});
});
$("#settingsForm input").on("keyup", function () {
validate($(this));
});
$("#settingsForm select").on("change", function () {
validate($(this));
});
function validate(object) {
let bool = check(object.prop("name"), object.prop("value"));
// Change class
if (bool) {
object.removeClass("is-invalid");
object.addClass("is-valid");
}
else {
object.removeClass("is-valid");
object.addClass("is-invalid");
}
return bool;
// Check values (return true/false)
function check(name, value) {
let check = validateChecks[name];
// If value is empty - check if it's optional
if (value.length == 0) {
if (check["optional"] == false) {
return false;
}
else {
object.prop("value", check["default"]);
return true;
}
}
// Check if value is too short
if (check["nmin"]) {
if (check["type"] == "int" || check["type"] == "float") {
if (value < check["nmin"]) {
return false;
}
}
else {
if (value.length < check["nmin"]) {
return false;
}
}
}
// Check if value is too long
if (check["nmax"]) {
if (check["type"] == "int" || check["type"] == "float") {
if (value > check["nmax"]) {
return false;
}
}
else {
if (value.length > check["nmax"]) {
return false;
}
}
}
// Check if value matches regex
if (check["regex"]) {
let regex = new RegExp(check["regex"]);
if (!(regex.test(value))) {
return false;
}
}
return true;
}
}
});
</script>
{% endblock %}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -39,12 +39,11 @@ The only original thing being done is the editing and gathering of all materials
1. Clone this repository 1. Clone this repository
2. Run `pip install -r requirements.txt` 2. Run `pip install -r requirements.txt`
3. Run `python -m playwright install` and `python -m playwright install-deps` 3. Run `python -m playwright install` and `python -m playwright install-deps`
**EXPERIMENTAL!!!!** **EXPERIMENTAL!!!!**
On MacOS and Linux (debian, arch, fedora and centos, and based on those), you can run an install script that will automatically install steps 1 to 3. (requires bash) On macOS and Linux (debian, arch, fedora and centos, and based on those), you can run an install script that will automatically install steps 1 to 3. (requires bash)
`bash <(curl -sL https://raw.githubusercontent.com/elebumm/RedditVideoMakerBot/master/install.sh)` `bash <(curl -sL https://raw.githubusercontent.com/elebumm/RedditVideoMakerBot/master/install.sh)`
@ -58,7 +57,7 @@ This can also be used to update the installation
(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) (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)
If you want to read more detailed guide about the bot, please refer to the [documentation](https://luka-hietala.gitbook.io/documentation-for-the-reddit-bot/) If you want to read more detailed guide about the bot, please refer to the [documentation](https://reddit-video-maker-bot.netlify.app/)
## Video ## Video

@ -1,9 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import random import random
from utils import settings
from gtts import gTTS from gtts import gTTS
max_chars = 0 from utils import settings
class GTTS: class GTTS:

@ -1,9 +1,11 @@
import base64 import base64
from utils import settings
import random import random
import requests import requests
from requests.adapters import HTTPAdapter, Retry from requests.adapters import HTTPAdapter, Retry
from utils import settings
# from profanity_filter import ProfanityFilter # from profanity_filter import ProfanityFilter
# pf = ProfanityFilter() # pf = ProfanityFilter()
# Code by @JasonLovesDoggo # Code by @JasonLovesDoggo
@ -62,9 +64,7 @@ noneng = [
class TikTok: # TikTok Text-to-Speech Wrapper class TikTok: # TikTok Text-to-Speech Wrapper
def __init__(self): def __init__(self):
self.URI_BASE = ( self.URI_BASE = "https://api16-normal-useast5.us.tiktokv.com/media/api/text/speech/invoke/?text_speaker="
"https://api16-normal-useast5.us.tiktokv.com/media/api/text/speech/invoke/?text_speaker="
)
self.max_chars = 300 self.max_chars = 300
self.voices = {"human": human, "nonhuman": nonhuman, "noneng": noneng} self.voices = {"human": human, "nonhuman": nonhuman, "noneng": noneng}
@ -75,10 +75,7 @@ class TikTok: # TikTok Text-to-Speech Wrapper
voice = ( voice = (
self.randomvoice() self.randomvoice()
if random_voice if random_voice
else ( else (settings.config["settings"]["tts"]["tiktok_voice"] or random.choice(self.voices["human"]))
settings.config["settings"]["tts"]["tiktok_voice"]
or random.choice(self.voices["human"])
)
) )
try: try:
r = requests.post(f"{self.URI_BASE}{voice}&req_text={text}&speaker_map_type=0") r = requests.post(f"{self.URI_BASE}{voice}&req_text={text}&speaker_map_type=0")

@ -1,9 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import random
import sys
from boto3 import Session from boto3 import Session
from botocore.exceptions import BotoCoreError, ClientError, ProfileNotFound from botocore.exceptions import BotoCoreError, ClientError, ProfileNotFound
import sys
from utils import settings from utils import settings
import random
voices = [ voices = [
"Brian", "Brian",
@ -37,15 +39,11 @@ class AWSPolly:
voice = self.randomvoice() voice = self.randomvoice()
else: else:
if not settings.config["settings"]["tts"]["aws_polly_voice"]: if not settings.config["settings"]["tts"]["aws_polly_voice"]:
raise ValueError( raise ValueError(f"Please set the TOML variable AWS_VOICE to a valid voice. options are: {voices}")
f"Please set the TOML variable AWS_VOICE to a valid voice. options are: {voices}"
)
voice = str(settings.config["settings"]["tts"]["aws_polly_voice"]).capitalize() voice = str(settings.config["settings"]["tts"]["aws_polly_voice"]).capitalize()
try: try:
# Request speech synthesis # Request speech synthesis
response = polly.synthesize_speech( response = polly.synthesize_speech(Text=text, OutputFormat="mp3", VoiceId=voice, Engine="neural")
Text=text, OutputFormat="mp3", VoiceId=voice, Engine="neural"
)
except (BotoCoreError, ClientError) as error: except (BotoCoreError, ClientError) as error:
# The service returned an error, exit gracefully # The service returned an error, exit gracefully
print(error) print(error)

@ -1,17 +1,22 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
import re
from pathlib import Path from pathlib import Path
from typing import Tuple from typing import Tuple
import re
# import sox # import sox
# from mutagen import MutagenError # from mutagen import MutagenError
# from mutagen.mp3 import MP3, HeaderNotFoundError # from mutagen.mp3 import MP3, HeaderNotFoundError
import numpy as np
import translators as ts import translators as ts
from moviepy.audio.AudioClip import AudioClip
from moviepy.audio.fx.volumex import volumex
from moviepy.editor import AudioFileClip
from rich.progress import track from rich.progress import track
from moviepy.editor import AudioFileClip, CompositeAudioClip, concatenate_audioclips
from utils import settings
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 utils import settings
DEFAULT_MAX_LENGTH: int = 50 # video length variable DEFAULT_MAX_LENGTH: int = 50 # video length variable
@ -70,9 +75,7 @@ class TTSEngine:
self.length -= self.last_clip_length self.length -= self.last_clip_length
idx -= 1 idx -= 1
break break
if ( if len(comment["comment_body"]) > self.tts_module.max_chars: # Split the comment if it is too long
len(comment["comment_body"]) > self.tts_module.max_chars
): # Split the comment if it is too long
self.split_post(comment["comment_body"], idx) # Split the comment self.split_post(comment["comment_body"], idx) # Split the comment
else: # If the comment is not too long, just call the tts engine else: # If the comment is not too long, just call the tts engine
self.call_tts(f"{idx}", process_text(comment["comment_body"])) self.call_tts(f"{idx}", process_text(comment["comment_body"]))
@ -83,50 +86,71 @@ class TTSEngine:
def split_post(self, text: str, idx: int): def split_post(self, text: str, idx: int):
split_files = [] split_files = []
split_text = [ split_text = [
x.group().strip() x.group().strip() for x in re.finditer(r" *(((.|\n){0," + str(self.tts_module.max_chars) + "})(\.|.$))", text)
for x in re.finditer(
r" *(((.|\n){0," + str(self.tts_module.max_chars) + "})(\.|.$))", text
)
] ]
offset = 0 self.create_silence_mp3()
for idy, text_cut in enumerate(split_text):
# print(f"{idx}-{idy}: {text_cut}\n")
new_text = process_text(text_cut)
if not new_text or new_text.isspace():
offset += 1
continue
self.call_tts(f"{idx}-{idy - offset}.part", new_text)
split_files.append(AudioFileClip(f"{self.path}/{idx}-{idy - offset}.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): idy = None
# print(f"Cleaning up {self.path}/{idx}-{i}.part.mp3") for idy, text_cut in enumerate(split_text):
newtext = process_text(text_cut)
# print(f"{idx}-{idy}: {newtext}\n")
# Path(f"{self.path}/{idx}-{i}.part.mp3").unlink() if not newtext or newtext.isspace():
print("newtext was blank because sanitized split text resulted in none")
continue
else:
self.call_tts(f"{idx}-{idy}.part", newtext)
with open(f"{self.path}/list.txt", 'w') as f:
for idz in range(0, len(split_text)):
f.write("file " + f"'{idx}-{idz}.part.mp3'" + "\n")
split_files.append(str(f"{self.path}/{idx}-{idy}.part.mp3"))
f.write("file " + f"'silence.mp3'" + "\n")
os.system("ffmpeg -f concat -y -hide_banner -loglevel panic -safe 0 " +
"-i " + f"{self.path}/list.txt " +
"-c copy " + f"{self.path}/{idx}.mp3")
try:
for i in range(0, len(split_files)):
os.unlink(split_files[i])
except FileNotFoundError as e:
print("File not found: " + e.filename)
except OSError:
print("OSError")
def call_tts(self, filename: str, text: str): def call_tts(self, filename: str, text: str):
self.tts_module.run(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")
try: try:
self.tts_module.run(text, filepath=f"{self.path}/{filename}_no_silence.mp3")
self.create_silence_mp3()
with open(f"{self.path}/{filename}.txt", 'w') as f:
f.write("file " + f"'{filename}_no_silence.mp3'" + "\n")
f.write("file " + f"'silence.mp3'" + "\n")
f.close()
os.system("ffmpeg -f concat -y -hide_banner -loglevel panic -safe 0 " +
"-i " + f"{self.path}/{filename}.txt " +
"-c copy " + f"{self.path}/{filename}.mp3")
clip = AudioFileClip(f"{self.path}/{filename}.mp3") clip = AudioFileClip(f"{self.path}/{filename}.mp3")
self.last_clip_length = clip.duration
self.length += clip.duration self.length += clip.duration
self.last_clip_length = clip.duration
clip.close() clip.close()
try:
name = [f"{filename}_no_silence.mp3", "silence.mp3", f"{filename}.txt"]
for i in range(0, len(name)):
os.unlink(str(rf"{self.path}/" + name[i]))
except FileNotFoundError as e:
print("File not found: " + e.filename)
except OSError:
print("OSError")
except: except:
self.length = 0 self.length = 0
def create_silence_mp3(self):
silence_duration = settings.config["settings"]["tts"]["silence_duration"]
silence = AudioClip(make_frame=lambda t: np.sin(440 * 2 * np.pi * t), duration=silence_duration, fps=44100)
silence = volumex(silence, 0)
silence.write_audiofile(f"{self.path}/silence.mp3", fps=44100, verbose=False, logger=None)
def process_text(text: str): def process_text(text: str):
lang = settings.config["reddit"]["thread"]["post_lang"] lang = settings.config["reddit"]["thread"]["post_lang"]

@ -1,5 +1,7 @@
import random import random
import pyttsx3 import pyttsx3
from utils import settings from utils import settings
@ -30,9 +32,7 @@ class pyttsx:
voice_id = self.randomvoice() voice_id = self.randomvoice()
engine = pyttsx3.init() engine = pyttsx3.init()
voices = engine.getProperty("voices") voices = engine.getProperty("voices")
engine.setProperty( engine.setProperty("voice", voices[voice_id].id) # changing index changes voices but ony 0 and 1 are working here
"voice", voices[voice_id].id
) # changing index changes voices but ony 0 and 1 are working here
engine.save_to_file(text, f"{filepath}") engine.save_to_file(text, f"{filepath}")
engine.runAndWait() engine.runAndWait()

@ -1,6 +1,8 @@
import random import random
import requests import requests
from requests.exceptions import JSONDecodeError from requests.exceptions import JSONDecodeError
from utils import settings from utils import settings
from utils.voice import check_ratelimit from utils.voice import check_ratelimit
@ -37,9 +39,7 @@ class StreamlabsPolly:
voice = self.randomvoice() voice = self.randomvoice()
else: else:
if not settings.config["settings"]["tts"]["streamlabs_polly_voice"]: if not settings.config["settings"]["tts"]["streamlabs_polly_voice"]:
raise ValueError( raise ValueError(f"Please set the config variable STREAMLABS_POLLY_VOICE to a valid voice. options are: {voices}")
f"Please set the config variable STREAMLABS_POLLY_VOICE to a valid voice. options are: {voices}"
)
voice = str(settings.config["settings"]["tts"]["streamlabs_polly_voice"]).capitalize() voice = str(settings.config["settings"]["tts"]["streamlabs_polly_voice"]).capitalize()
body = {"voice": voice, "text": text, "service": "polly"} body = {"voice": voice, "text": text, "service": "polly"}
response = requests.post(self.url, data=body) response = requests.post(self.url, data=body)

@ -62,7 +62,13 @@ function install_macos(){
fi fi
# Install the required packages # Install the required packages
echo "Installing required Packages" echo "Installing required Packages"
brew install python@3.10 tcl-tk python-tk if [! command --version python3 &> /dev/null ]; then
echo "Installing python3"
brew install python@3.10
else
echo "python3 already installed."
fi
brew install tcl-tk python-tk
} }
# Function to install for arch (and other forks like manjaro) # Function to install for arch (and other forks like manjaro)

@ -1,18 +1,18 @@
#!/usr/bin/env python #!/usr/bin/env python
import math import math
import re import sys
from subprocess import Popen
from os import name from os import name
from subprocess import Popen
from pathlib import Path
from prawcore import ResponseException from prawcore import ResponseException
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, print_substep
from utils import settings from utils import settings
from utils.cleanup import cleanup
from utils.console import print_markdown, print_step
from utils.id import id from utils.id import id
from utils.version import checkversion from utils.version import checkversion
from video_creation.background import ( from video_creation.background import (
download_background, download_background,
chop_background_video, chop_background_video,
@ -22,7 +22,7 @@ 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.4.1" __VERSION__ = "2.4.2"
print( print(
""" """
@ -36,7 +36,7 @@ print(
) )
# 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. You can find solutions to many common problems in the [Documentation](https://luka-hietala.gitbook.io/documentation-for-the-reddit-bot/)" "### 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://reddit-video-maker-bot.netlify.app/"
) )
checkversion(__VERSION__) checkversion(__VERSION__)
@ -75,14 +75,14 @@ def shutdown():
print("Exiting...") print("Exiting...")
exit() exit()
if __name__ == "__main__": if __name__ == "__main__":
config = settings.check_toml("utils/.config.template.toml", "config.toml") assert sys.version_info >= (3, 9), "Python 3.10 or higher is required"
directory = Path().absolute()
config = settings.check_toml(f"{directory}/utils/.config.template.toml", "config.toml")
config is False and exit() config is False and exit()
try: try:
if config["settings"]["times_to_run"]: if len(config["reddit"]["thread"]["post_id"].split("+")) > 1:
run_many(config["settings"]["times_to_run"])
elif len(config["reddit"]["thread"]["post_id"].split("+")) > 1:
for index, post_id in enumerate(config["reddit"]["thread"]["post_id"].split("+")): for index, post_id in enumerate(config["reddit"]["thread"]["post_id"].split("+")):
index += 1 index += 1
print_step( print_step(
@ -90,6 +90,8 @@ if __name__ == "__main__":
) )
main(post_id) main(post_id)
Popen("cls" if name == "nt" else "clear", shell=True).wait() Popen("cls" if name == "nt" else "clear", shell=True).wait()
elif config["settings"]["times_to_run"]:
run_many(config["settings"]["times_to_run"])
else: else:
main() main()
except KeyboardInterrupt: except KeyboardInterrupt:

@ -1,11 +1,10 @@
import re import re
from prawcore.exceptions import ResponseException
from utils import settings
import praw import praw
from praw.models import MoreComments from praw.models import MoreComments
from prawcore.exceptions import ResponseException
from utils import settings
from utils.console import print_step, print_substep from utils.console import print_step, print_substep
from utils.subreddit import get_subreddit_undone from utils.subreddit import get_subreddit_undone
from utils.videos import check_done from utils.videos import check_done
@ -41,8 +40,7 @@ def get_subreddit_threads(POST_ID: str):
check_for_async=False, check_for_async=False,
) )
except ResponseException as e: except ResponseException as e:
match e.response.status_code: if e.response.status_code == 401:
case 401:
print("Invalid credentials - please check them in config.toml") print("Invalid credentials - please check them in config.toml")
except: except:
print("Something went wrong...") print("Something went wrong...")
@ -105,12 +103,9 @@ def get_subreddit_threads(POST_ID: str):
sanitised = sanitize_text(top_level_comment.body) sanitised = sanitize_text(top_level_comment.body)
if not sanitised or sanitised == " ": if not sanitised or sanitised == " ":
continue continue
if len(top_level_comment.body) <= int( if len(top_level_comment.body) <= int(settings.config["reddit"]["thread"]["max_comment_length"]):
settings.config["reddit"]["thread"]["max_comment_length"]
):
if ( if (
top_level_comment.author is not None top_level_comment.author is not None and sanitize_text(top_level_comment.body) is not None
and sanitize_text(top_level_comment.body) is not None
): # if errors occur with this change to if not. ): # if errors occur with this change to if not.
content["comments"].append( content["comments"].append(
{ {

@ -12,3 +12,5 @@ toml==0.10.2
translators==5.3.1 translators==5.3.1
pyttsx3==2.90 pyttsx3==2.90
Pillow~=9.1.1 Pillow~=9.1.1
tomlkit==0.11.4
Flask==2.2.2

@ -25,15 +25,16 @@ storymode = { optional = true, type = "bool", default = false, example = false,
[settings.background] [settings.background]
background_choice = { optional = true, default = "minecraft", example = "rocket-league", options = ["minecraft", "gta", "rocket-league", "motor-gta", "csgo-surf", "cluster-truck", ""], explanation = "Sets the background for the video based on game name" } background_choice = { optional = true, default = "minecraft", example = "rocket-league", options = ["", "minecraft", "gta", "rocket-league", "motor-gta", "csgo-surf", "cluster-truck"], explanation = "Sets the background for the video based on game name" }
#background_audio = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Sets a audio to play in the background (put a background.mp3 file in the assets/backgrounds directory for it to be used.)" } #background_audio = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Sets a audio to play in the background (put a background.mp3 file in the assets/backgrounds directory for it to be used.)" }
#background_audio_volume = { optional = true, type = "float", default = 0.3, example = 0.1, explanation="Sets the volume of the background audio. only used if the background_audio is also set to true" } #background_audio_volume = { optional = true, type = "float", default = 0.3, example = 0.1, explanation="Sets the volume of the background audio. only used if the background_audio is also set to true" }
[settings.tts] [settings.tts]
voice_choice = { optional = false, default = "", options = ["streamlabspolly", "tiktok", "googletranslate", "awspolly", "pyttsx",], example = "tiktok", explanation = "The voice platform used for TTS generation. This can be left blank and you will be prompted to choose at runtime." } voice_choice = { optional = false, default = "tiktok", options = ["streamlabspolly", "tiktok", "googletranslate", "awspolly", "pyttsx", ], example = "tiktok", explanation = "The voice platform used for TTS generation. This can be left blank and you will be prompted to choose at runtime." }
aws_polly_voice = { optional = false, default = "Matthew", example = "Matthew", explanation = "The voice used for AWS Polly" } aws_polly_voice = { optional = false, default = "Matthew", example = "Matthew", explanation = "The voice used for AWS Polly" }
streamlabs_polly_voice = { optional = false, default = "Matthew", example = "Matthew", explanation = "The voice used for Streamlabs Polly" } streamlabs_polly_voice = { optional = false, default = "Matthew", example = "Matthew", explanation = "The voice used for Streamlabs Polly" }
tiktok_voice = { optional = false, default = "en_us_006", example = "en_us_006", explanation = "The voice used for TikTok TTS" } tiktok_voice = { optional = false, default = "en_us_006", example = "en_us_006", explanation = "The voice used for TikTok TTS" }
python_voice = { optional = false, default = "1", example = "1", explanation = "The index of the system tts voices (can be downloaded externally, run ptt.py to find value, start from zero)" } python_voice = { optional = false, default = "1", example = "1", explanation = "The index of the system tts voices (can be downloaded externally, run ptt.py to find value, start from zero)" }
py_voice_num = {optional = false, default = "2", example = "2", explanation= "the number of system voices(2 are pre-installed in windows)"} py_voice_num = { optional = false, default = "2", example = "2", explanation = "The number of system voices (2 are pre-installed in Windows)" }
silence_duration = { optional = true, example = "0.1", explanation = "Time in seconds between TTS comments", default = 0.3, type = "float" }

@ -1,45 +0,0 @@
# Supported Background. Can add/remove background video here....
# <key>-<value> : key -> used as keyword for TOML file. value -> background configuration
# Format (value):
# 1. Youtube URI
# 2. filename
# 3. Citation (owner of the video)
# 4. Position of image clips in the background. See moviepy reference for more information. (https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html#moviepy.video.VideoClip.VideoClip.set_position)
background_options = {
"motor-gta": ( # Motor-GTA Racing
"https://www.youtube.com/watch?v=vw5L4xCPy9Q",
"bike-parkour-gta.mp4",
"Achy Gaming",
lambda t: ("center", 480 + t),
),
"rocket-league": ( # Rocket League
"https://www.youtube.com/watch?v=2X9QGY__0II",
"rocket_league.mp4",
"Orbital Gameplay",
lambda t: ("center", 200 + t),
),
"minecraft": ( # Minecraft parkour
"https://www.youtube.com/watch?v=n_Dv4JMiwK8",
"parkour.mp4",
"bbswitzer",
"center",
),
"gta": ( # GTA Stunt Race
"https://www.youtube.com/watch?v=qGa9kWREOnE",
"gta-stunt-race.mp4",
"Achy Gaming",
lambda t: ("center", 480 + t),
),
"csgo-surf": ( # CSGO Surf
"https://www.youtube.com/watch?v=E-8JlyO59Io",
"csgo-surf.mp4",
"Aki",
"center",
),
"cluster-truck": ( # Cluster Truck Gameplay
"https://www.youtube.com/watch?v=uVKxtdMgJVU",
"cluster_truck.mp4",
"No Copyright Gameplay",
lambda t: ("center", 480 + t),
),
}

@ -0,0 +1,39 @@
{
"__comment": "Supported Backgrounds. Can add/remove background video here...",
"motor-gta": [
"https://www.youtube.com/watch?v=vw5L4xCPy9Q",
"bike-parkour-gta.mp4",
"Achy Gaming",
480
],
"rocket-league": [
"https://www.youtube.com/watch?v=2X9QGY__0II",
"rocket_league.mp4",
"Orbital Gameplay",
200
],
"minecraft": [
"https://www.youtube.com/watch?v=n_Dv4JMiwK8",
"parkour.mp4",
"bbswitzer",
"center"
],
"gta": [
"https://www.youtube.com/watch?v=qGa9kWREOnE",
"gta-stunt-race.mp4",
"Achy Gaming",
480
],
"csgo-surf": [
"https://www.youtube.com/watch?v=E-8JlyO59Io",
"csgo-surf.mp4",
"Aki",
"center"
],
"cluster-truck": [
"https://www.youtube.com/watch?v=uVKxtdMgJVU",
"cluster_truck.mp4",
"No Copyright Gameplay",
480
]
}

@ -1,11 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import re
from rich.columns import Columns
from rich.console import Console from rich.console import Console
from rich.markdown import Markdown 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()
@ -54,11 +55,7 @@ def handle_input(
return default if default is not NotImplemented else "" return default if default is not NotImplemented else ""
if default is not NotImplemented: if default is not NotImplemented:
console.print( console.print(
"[green]" "[green]" + message + '\n[blue bold]The default value is "' + str(default) + '"\nDo you want to use it?(y/n)'
+ message
+ '\n[blue bold]The default value is "'
+ str(default)
+ '"\nDo you want to use it?(y/n)'
) )
if input().casefold().startswith("y"): if input().casefold().startswith("y"):
return default return default
@ -71,9 +68,7 @@ def handle_input(
if check_type is not False: if check_type is not False:
try: try:
user_input = check_type(user_input) user_input = check_type(user_input)
if (nmin is not None and user_input < nmin) or ( if (nmin is not None and user_input < nmin) or (nmax is not None and user_input > nmax):
nmax is not None and user_input > nmax
):
# FAILSTATE Input out of bounds # FAILSTATE Input out of bounds
console.print("[red]" + oob_error) console.print("[red]" + oob_error)
continue continue
@ -89,9 +84,7 @@ def handle_input(
continue continue
else: else:
# FAILSTATE Input STRING out of bounds # FAILSTATE Input STRING out of bounds
if (nmin is not None and len(user_input) < nmin) or ( if (nmin is not None and len(user_input) < nmin) or (nmax is not None and len(user_input) > nmax):
nmax is not None and len(user_input) > nmax
):
console.print("[red bold]" + oob_error) console.print("[red bold]" + oob_error)
continue continue
break # SUCCESS Input STRING in bounds break # SUCCESS Input STRING in bounds
@ -105,16 +98,8 @@ def handle_input(
isinstance(eval(user_input), check_type) isinstance(eval(user_input), check_type)
return check_type(user_input) return check_type(user_input)
except: except:
console.print( console.print("[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + ".")
"[red bold]"
+ err_message
+ "\nValid options are: "
+ ", ".join(map(str, options))
+ "."
)
continue continue
if user_input in options: if user_input in options:
return user_input return user_input
console.print( console.print("[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + ".")
"[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "."
)

@ -0,0 +1,226 @@
import json
import re
from pathlib import Path
import toml
import tomlkit
from flask import flash
# Get validation checks from template
def get_checks():
template = toml.load("utils/.config.template.toml")
checks = {}
def unpack_checks(obj: dict):
for key in obj.keys():
if "optional" in obj[key].keys():
checks[key] = obj[key]
else:
unpack_checks(obj[key])
unpack_checks(template)
return checks
# Get current config (from config.toml) as dict
def get_config(obj: dict, done={}):
for key in obj.keys():
if not isinstance(obj[key], dict):
done[key] = obj[key]
else:
get_config(obj[key], done)
return done
# Checks if value is valid
def check(value, checks):
incorrect = False
if value == "False":
value = ""
if not incorrect and "type" in checks:
try:
value = eval(checks["type"])(value)
except Exception:
incorrect = True
if (
not incorrect and "options" in checks and value not in checks["options"]
): # FAILSTATE Value is not one of the options
incorrect = True
if (
not incorrect
and "regex" in checks
and (
(isinstance(value, str) and re.match(checks["regex"], value) is None)
or not isinstance(value, str)
)
): # FAILSTATE Value doesn't match regex, or has regex but is not a string.
incorrect = True
if (
not incorrect
and not hasattr(value, "__iter__")
and (
("nmin" in checks and checks["nmin"] is not None and value < checks["nmin"])
or (
"nmax" in checks
and checks["nmax"] is not None
and value > checks["nmax"]
)
)
):
incorrect = True
if (
not incorrect
and hasattr(value, "__iter__")
and (
(
"nmin" in checks
and checks["nmin"] is not None
and len(value) < checks["nmin"]
)
or (
"nmax" in checks
and checks["nmax"] is not None
and len(value) > checks["nmax"]
)
)
):
incorrect = True
if incorrect:
return "Error"
return value
# Modify settings (after form is submitted)
def modify_settings(data: dict, config_load, checks: dict):
# Modify config settings
def modify_config(obj: dict, name: str, value: any):
for key in obj.keys():
if name == key:
obj[key] = value
elif not isinstance(obj[key], dict):
continue
else:
modify_config(obj[key], name, value)
# Remove empty/incorrect key-value pairs
data = {key: value for key, value in data.items() if value and key in checks.keys()}
# Validate values
for name in data.keys():
value = check(data[name], checks[name])
# Value is invalid
if value == "Error":
flash("Some values were incorrect and didn't save!", "error")
else:
# Value is valid
modify_config(config_load, name, value)
# Save changes in config.toml
with Path("config.toml").open("w") as toml_file:
toml_file.write(tomlkit.dumps(config_load))
flash("Settings saved!")
return get_config(config_load)
# Delete background video
def delete_background(key):
# Read backgrounds.json
with open("utils/backgrounds.json", "r", encoding="utf-8") as backgrounds:
data = json.load(backgrounds)
# Remove background from backgrounds.json
with open("utils/backgrounds.json", "w", encoding="utf-8") as backgrounds:
if data.pop(key, None):
json.dump(data, backgrounds, ensure_ascii=False, indent=4)
else:
flash("Couldn't find this background. Try refreshing the page.", "error")
return
# Remove background video from ".config.template.toml"
config = tomlkit.loads(Path("utils/.config.template.toml").read_text())
config["settings"]["background"]["background_choice"]["options"].remove(key)
with Path("utils/.config.template.toml").open("w") as toml_file:
toml_file.write(tomlkit.dumps(config))
flash(f'Successfully removed "{key}" background!')
# Add background video
def add_background(youtube_uri, filename, citation, position):
# Validate YouTube URI
regex = re.compile(r"(?:\/|%3D|v=|vi=)([0-9A-z-_]{11})(?:[%#?&]|$)").search(
youtube_uri
)
if not regex:
flash("YouTube URI is invalid!", "error")
return
youtube_uri = f"https://www.youtube.com/watch?v={regex.group(1)}"
# Check if position is valid
if position == "" or position == "center":
position = "center"
elif position.isdecimal():
position = int(position)
else:
flash('Position is invalid! It can be "center" or decimal number.', "error")
return
# Sanitize filename
regex = re.compile(r"^([a-zA-Z0-9\s_-]{1,100})$").match(filename)
if not regex:
flash("Filename is invalid!", "error")
return
filename = filename.replace(" ", "_")
# Check if background doesn't already exist
with open("utils/backgrounds.json", "r", encoding="utf-8") as backgrounds:
data = json.load(backgrounds)
# Check if key isn't already taken
if filename in list(data.keys()):
flash("Background video with this name already exist!", "error")
return
# Check if the YouTube URI isn't already used under different name
if youtube_uri in [data[i][0] for i in list(data.keys())]:
flash("Background video with this YouTube URI is already added!", "error")
return
# Add background video to json file
with open("utils/backgrounds.json", "r+", encoding="utf-8") as backgrounds:
data = json.load(backgrounds)
data[filename] = [youtube_uri, filename + ".mp4", citation, position]
backgrounds.seek(0)
json.dump(data, backgrounds, ensure_ascii=False, indent=4)
# Add background video to ".config.template.toml"
config = tomlkit.loads(Path("utils/.config.template.toml").read_text())
config["settings"]["background"]["background_choice"]["options"].append(filename)
with Path("utils/.config.template.toml").open("w") as toml_file:
toml_file.write(tomlkit.dumps(config))
flash(f'Added "{citation}-{filename}.mp4" as a new background video!')
return

@ -1,6 +1,8 @@
import re import re
from utils.console import print_substep from utils.console import print_substep
def id(reddit_obj: dict): def id(reddit_obj: dict):
""" """
This function takes a reddit object and returns the post id This function takes a reddit object and returns the post id

@ -1,13 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python
import toml
from rich.console import Console
import re import re
from typing import Tuple, Dict from typing import Tuple, Dict
from pathlib import Path
import toml
from rich.console import Console
from utils.console import handle_input from utils.console import handle_input
console = Console() console = Console()
config = dict # autocomplete config = dict # autocomplete
@ -35,17 +34,12 @@ def check(value, checks, name):
except: except:
incorrect = True incorrect = True
if ( if not incorrect and "options" in checks and value not in checks["options"]: # FAILSTATE Value is not one of the options
not incorrect and "options" in checks and value not in checks["options"]
): # FAILSTATE Value is not one of the options
incorrect = True incorrect = True
if ( if (
not incorrect not incorrect
and "regex" in checks and "regex" in checks
and ( and ((isinstance(value, str) and re.match(checks["regex"], value) is None) or not isinstance(value, str))
(isinstance(value, str) and re.match(checks["regex"], value) is None)
or not isinstance(value, str)
)
): # FAILSTATE Value doesn't match regex, or has regex but is not a string. ): # FAILSTATE Value doesn't match regex, or has regex but is not a string.
incorrect = True incorrect = True
@ -85,9 +79,7 @@ def check(value, checks, name):
err_message=get_check_value("input_error", "Incorrect input"), err_message=get_check_value("input_error", "Incorrect input"),
nmin=get_check_value("nmin", None), nmin=get_check_value("nmin", None),
nmax=get_check_value("nmax", None), nmax=get_check_value("nmax", None),
oob_error=get_check_value( oob_error=get_check_value("oob_error", "Input out of bounds(Value too high/low/long/short)"),
"oob_error", "Input out of bounds(Value too high/low/long/short)"
),
options=get_check_value("options", None), options=get_check_value("options", None),
optional=get_check_value("optional", False), optional=get_check_value("optional", False),
) )
@ -167,4 +159,5 @@ If you see any prompts, that means that you have unset/incorrectly set variables
if __name__ == "__main__": if __name__ == "__main__":
check_toml("utils/.config.template.toml", "config.toml") directory = Path().absolute()
check_toml(f"{directory}/utils/.config.template.toml", "config.toml")

@ -54,9 +54,7 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0):
print("all time filters have been checked you absolute madlad ") print("all time filters have been checked you absolute madlad ")
return get_subreddit_undone( return get_subreddit_undone(
subreddit.top( subreddit.top(time_filter=VALID_TIME_FILTERS[index], limit=(50 if int(index) == 0 else index + 1 * 50)),
time_filter=VALID_TIME_FILTERS[index], limit=(50 if int(index) == 0 else index + 1 * 50)
),
subreddit, subreddit,
times_checked=index, times_checked=index,
) # all the videos in hot have already been done ) # all the videos in hot have already been done

@ -1,11 +1,10 @@
import requests import requests
from utils.console import print_step from utils.console import print_step
def checkversion(__VERSION__): def checkversion(__VERSION__):
response = requests.get( response = requests.get("https://api.github.com/repos/elebumm/RedditVideoMakerBot/releases/latest")
"https://api.github.com/repos/elebumm/RedditVideoMakerBot/releases/latest"
)
latestversion = response.json()["tag_name"] latestversion = response.json()["tag_name"]
if __VERSION__ == latestversion: if __VERSION__ == latestversion:
print_step(f"You are using the newest version ({__VERSION__}) of the bot") print_step(f"You are using the newest version ({__VERSION__}) of the bot")

@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
from ast import Str
import re
import re
from typing import Tuple from typing import Tuple
from PIL import ImageFont, Image, ImageDraw, ImageEnhance from PIL import ImageFont, Image, ImageDraw, ImageEnhance
@ -37,9 +36,7 @@ class Video:
im.save(path) im.save(path)
return ImageClip(path) return ImageClip(path)
def add_watermark( def add_watermark(self, text, redditid, opacity=0.5, duration: int | float = 5, position: Tuple = (0.7, 0.9), fontsize=15):
self, text, redditid, opacity=0.5, duration: int | float = 5, position: Tuple = (0.7, 0.9), fontsize=15
):
compensation = round( compensation = round(
(position[0] / ((len(text) * (fontsize / 5) / 1.5) / 100 + position[0] * position[0])), (position[0] / ((len(text) * (fontsize / 5) / 1.5) / 100 + position[0] * position[0])),
ndigits=2, ndigits=2,

@ -1,6 +1,5 @@
import json import json
import time import time
from typing import Dict
from praw.models import Submission from praw.models import Submission

@ -1,7 +1,7 @@
import re import re
import sys import sys
from datetime import datetime
import time as pytime import time as pytime
from datetime import datetime
from time import sleep from time import sleep
from requests import Response from requests import Response
@ -81,7 +81,7 @@ def sanitize_text(text: str) -> str:
result = re.sub(regex_urls, " ", text) result = re.sub(regex_urls, " ", text)
# note: not removing apostrophes # note: not removing apostrophes
regex_expr = r"\s['|]|['|]\s|[\^_~@!&;#:\-–—%“”‘\"%\*/{}\[\]\(\)\\|<>=+]" regex_expr = r"\s['|]|['|]\s|[\^_~@!&;#:\-%“”‘\"%\*/{}\[\]\(\)\\|<>=+]"
result = re.sub(regex_expr, " ", result) result = re.sub(regex_expr, " ", result)
result = result.replace("+", "plus").replace("&", "and") result = result.replace("+", "plus").replace("&", "and")
# remove extra whitespace # remove extra whitespace

@ -1,19 +1,32 @@
from pathlib import Path import json
import random import random
from random import randrange
import re import re
from pathlib import Path
from random import randrange
from typing import Any, Tuple from typing import Any, Tuple
from moviepy.editor import VideoFileClip from moviepy.editor import VideoFileClip
from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
from pytube import YouTube from pytube import YouTube
from pytube.cli import on_progress from pytube.cli import on_progress
from utils import settings from utils import settings
from utils.CONSTANTS import background_options
from utils.console import print_step, print_substep from utils.console import print_step, print_substep
# Load background videos
with open("utils/backgrounds.json") as json_file:
background_options = json.load(json_file)
# Remove "__comment" from backgrounds
background_options.pop("__comment", None)
# Add position lambda function
# (https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html#moviepy.video.VideoClip.VideoClip.set_position)
for name in list(background_options.keys()):
pos = background_options[name][3]
if pos != "center":
background_options[name][3] = lambda t: ("center", pos + t)
def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int, int]: 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. """Generates a random interval of time to be used as the background of the video.
@ -52,9 +65,7 @@ def download_background(background_config: Tuple[str, str, str, Any]):
uri, filename, credit, _ = background_config uri, filename, credit, _ = background_config
if Path(f"assets/backgrounds/{credit}-{filename}").is_file(): if Path(f"assets/backgrounds/{credit}-{filename}").is_file():
return return
print_step( print_step("We need to download the backgrounds videos. they are fairly large but it's only done once. 😎")
"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 🙏 ") print_substep("Downloading the backgrounds videos... please be patient 🙏 ")
print_substep(f"Downloading {filename} from {uri}") print_substep(f"Downloading {filename} from {uri}")
YouTube(uri, on_progress_callback=on_progress).streams.filter(res="1080p").first().download( YouTube(uri, on_progress_callback=on_progress).streams.filter(res="1080p").first().download(

@ -4,6 +4,7 @@ import os
import re import re
from os.path import exists from os.path import exists
from typing import Tuple, Any from typing import Tuple, Any
from moviepy.audio.AudioClip import concatenate_audioclips, CompositeAudioClip from moviepy.audio.AudioClip import concatenate_audioclips, CompositeAudioClip
from moviepy.audio.io.AudioFileClip import AudioFileClip from moviepy.audio.io.AudioFileClip import AudioFileClip
from moviepy.video.VideoClip import ImageClip from moviepy.video.VideoClip import ImageClip
@ -13,11 +14,11 @@ from moviepy.video.io.VideoFileClip import VideoFileClip
from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
from rich.console import Console from rich.console import Console
from utils import settings
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 utils.video import Video from utils.video import Video
from utils.videos import save_data from utils.videos import save_data
from utils import settings
console = Console() console = Console()
W, H = 1080, 1920 W, H = 1080, 1920
@ -118,9 +119,7 @@ def make_final_video(
# ) # )
# else: story mode stuff # else: story mode stuff
img_clip_pos = background_config[3] img_clip_pos = background_config[3]
image_concat = concatenate_videoclips(image_clips).set_position( image_concat = concatenate_videoclips(image_clips).set_position(img_clip_pos) # note transition kwarg for delay in imgs
img_clip_pos
) # note transition kwarg for delay in imgs
image_concat.audio = audio_composite image_concat.audio = audio_composite
final = CompositeVideoClip([background_clip, image_concat]) final = CompositeVideoClip([background_clip, image_concat])
title = re.sub(r"[^\w\s-]", "", reddit_obj["thread_title"]) title = re.sub(r"[^\w\s-]", "", reddit_obj["thread_title"])
@ -140,9 +139,7 @@ def make_final_video(
# # lowered_audio = audio_background.multiply_volume( # todo get this to work # # lowered_audio = audio_background.multiply_volume( # todo get this to work
# # VOLUME_MULTIPLIER) # lower volume by background_audio_volume, use with fx # # VOLUME_MULTIPLIER) # lower volume by background_audio_volume, use with fx
# final.set_audio(final_audio) # final.set_audio(final_audio)
final = Video(final).add_watermark( final = Video(final).add_watermark(text=f"Background credit: {background_config[2]}", opacity=0.4, redditid=reddit_obj)
text=f"Background credit: {background_config[2]}", opacity=0.4, redditid=reddit_obj
)
final.write_videofile( final.write_videofile(
f"assets/temp/{id}/temp.mp4", f"assets/temp/{id}/temp.mp4",
fps=30, fps=30,
@ -163,6 +160,4 @@ def make_final_video(
print_substep(f"Removed {cleanups} temporary files 🗑") print_substep(f"Removed {cleanups} temporary files 🗑")
print_substep("See result in the results folder!") print_substep("See result in the results folder!")
print_step( print_step(f'Reddit title: {reddit_obj["thread_title"]} \n Background Credit: {background_config[2]}')
f'Reddit title: {reddit_obj["thread_title"]} \n Background Credit: {background_config[2]}'
)

@ -1,19 +1,17 @@
import json import json
from pathlib import Path
import re import re
from pathlib import Path
from typing import Dict from typing import Dict
from utils import settings
from playwright.async_api import async_playwright # pylint: disable=unused-import
# do not remove the above line
import translators as ts
from playwright.sync_api import sync_playwright, ViewportSize from playwright.sync_api import sync_playwright, ViewportSize
from rich.progress import track from rich.progress import track
import translators as ts
from utils import settings
from utils.console import print_step, print_substep from utils.console import print_step, print_substep
# do not remove the above line
storymode = False storymode = False
@ -32,7 +30,7 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in
with sync_playwright() as p: with sync_playwright() as p:
print_substep("Launching Headless Browser...") print_substep("Launching Headless Browser...")
browser = p.chromium.launch() browser = p.chromium.launch(headless=True) # add headless=False for debug
context = browser.new_context() context = browser.new_context()
if settings.config["settings"]["theme"] == "dark": if settings.config["settings"]["theme"] == "dark":
@ -53,9 +51,7 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in
page.wait_for_load_state() # Wait for page to fully load page.wait_for_load_state() # Wait for page to fully load
if page.locator('[data-click-id="text"] button').is_visible(): if page.locator('[data-click-id="text"] button').is_visible():
page.locator( page.locator('[data-click-id="text"] button').click() # Remove "Click to see nsfw" Button in Screenshot
'[data-click-id="text"] button'
).click() # Remove "Click to see nsfw" Button in Screenshot
# translate code # translate code
@ -77,13 +73,9 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in
page.locator('[data-test-id="post-content"]').screenshot(path=postcontentpath) page.locator('[data-test-id="post-content"]').screenshot(path=postcontentpath)
if storymode: if storymode:
page.locator('[data-click-id="text"]').screenshot( page.locator('[data-click-id="text"]').screenshot(path=f"assets/temp/{id}/png/story_content.png")
path=f"assets/temp/{id}/png/story_content.png"
)
else: else:
for idx, comment in enumerate( for idx, comment in enumerate(track(reddit_object["comments"], "Downloading screenshots...")):
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
@ -105,9 +97,7 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in
[comment_tl, comment["comment_id"]], [comment_tl, comment["comment_id"]],
) )
try: try:
page.locator(f"#t1_{comment['comment_id']}").screenshot( page.locator(f"#t1_{comment['comment_id']}").screenshot(path=f"assets/temp/{id}/png/comment_{idx}.png")
path=f"assets/temp/{id}/png/comment_{idx}.png"
)
except TimeoutError: except TimeoutError:
del reddit_object["comments"] del reddit_object["comments"]
screenshot_num += 1 screenshot_num += 1

@ -1,19 +1,18 @@
#!/usr/bin/env python #!/usr/bin/env python
from typing import Dict, Tuple from typing import Tuple
from rich.console import Console from rich.console import Console
from TTS.engine_wrapper import TTSEngine
from TTS.GTTS import GTTS from TTS.GTTS import GTTS
from TTS.streamlabs_polly import StreamlabsPolly
from TTS.aws_polly import AWSPolly
from TTS.TikTok import TikTok from TTS.TikTok import TikTok
from TTS.aws_polly import AWSPolly
from TTS.engine_wrapper import TTSEngine
from TTS.pyttsx import pyttsx from TTS.pyttsx import pyttsx
from TTS.streamlabs_polly import StreamlabsPolly
from utils import settings from utils import settings
from utils.console import print_table, print_step from utils.console import print_table, print_step
console = Console() console = Console()
TTSProviders = { TTSProviders = {

Loading…
Cancel
Save