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]
App version / Branch [e.g. latest, V2.0, master, develop]
App version / Branch : [e.g. latest, V2.0, master, develop]
validations:
required: true
- type: checkboxes

@ -1,4 +1,4 @@
blank_issues_enabled: true
blank_issues_enabled: false
contact_links:
- name: Ask a question
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
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
> 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.
@ -144,4 +144,4 @@ When making your PR, follow these guidelines:
### 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
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
HOST = "localhost"
# Set the port number
PORT = 4000
# Define class to display the index page of the web server
class PythonServer(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == "/GUI":
self.path = "index.html"
return http.server.SimpleHTTPRequestHandler.do_GET(self)
# Declare object of the class
webServer = http.server.HTTPServer((HOST, PORT), PythonServer)
# Print the URL of the webserver, new =2 opens in a new tab
print(f"Server started at http://{HOST}:{PORT}/GUI/")
webbrowser.open(f"http://{HOST}:{PORT}/GUI/", new=2)
print("Website opened in new tab")
print("Press Ctrl+C to quit")
try:
# Run the web server
webServer.serve_forever()
except KeyboardInterrupt:
# Stop the web server
webServer.server_close()
print("The server is stopped.")
exit()
# Configure application
app = Flask(__name__, template_folder="GUI")
# Configure secret key only to use 'flash'
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
# Ensure responses aren't cached
@app.after_request
def after_request(response):
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
response.headers["Expires"] = 0
response.headers["Pragma"] = "no-cache"
return response
# Display index.html
@app.route("/")
def index():
return render_template("index.html", file="videos.json")
@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">
<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.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>
{% extends "layout.html" %}
{% block main %}
<main>
<div class="album py-2 bg-light">
@ -89,7 +7,8 @@
<div class="row mt-2">
<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>
@ -100,22 +19,6 @@
</div>
</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>
const intervals = [
{ label: 'year', seconds: 31536000 },
@ -134,12 +37,11 @@
}
$(document).ready(function () {
$.getJSON("../video_creation/data/videos.json",
$.getJSON("videos.json",
function (data) {
data.sort((b, a) => a['time'] - b['time'])
var video = '';
$.each(data, function (key, value) {
video += '<div class="col">';
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>';
@ -263,7 +165,7 @@
}
var searchFilter = () => {
const input = document.querySelector(".form-control");
const input = document.querySelector(".searchFilter");
const cards = document.getElementsByClassName("col");
console.log(cards[1])
let filter = input.value
@ -277,5 +179,4 @@
}
}
</script>
</body>
</html>
{% endblock %}

@ -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
2. Run `pip install -r requirements.txt`
3. Run `python -m playwright install` and `python -m playwright install-deps`
**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)`
@ -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)
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

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

@ -1,9 +1,11 @@
import base64
from utils import settings
import random
import requests
from requests.adapters import HTTPAdapter, Retry
from utils import settings
# from profanity_filter import ProfanityFilter
# pf = ProfanityFilter()
# Code by @JasonLovesDoggo
@ -62,9 +64,7 @@ noneng = [
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.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}
@ -75,10 +75,7 @@ class TikTok: # TikTok Text-to-Speech Wrapper
voice = (
self.randomvoice()
if random_voice
else (
settings.config["settings"]["tts"]["tiktok_voice"]
or random.choice(self.voices["human"])
)
else (settings.config["settings"]["tts"]["tiktok_voice"] or random.choice(self.voices["human"]))
)
try:
r = requests.post(f"{self.URI_BASE}{voice}&req_text={text}&speaker_map_type=0")

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

@ -1,17 +1,22 @@
#!/usr/bin/env python3
import os
import re
from pathlib import Path
from typing import Tuple
import re
# import sox
# from mutagen import MutagenError
# from mutagen.mp3 import MP3, HeaderNotFoundError
import numpy as np
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 moviepy.editor import AudioFileClip, CompositeAudioClip, concatenate_audioclips
from utils import settings
from utils.console import print_step, print_substep
from utils.voice import sanitize_text
from utils import settings
DEFAULT_MAX_LENGTH: int = 50 # video length variable
@ -70,9 +75,7 @@ class TTSEngine:
self.length -= self.last_clip_length
idx -= 1
break
if (
len(comment["comment_body"]) > self.tts_module.max_chars
): # Split the comment if it is too long
if 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
else: # If the comment is not too long, just call the tts engine
self.call_tts(f"{idx}", process_text(comment["comment_body"]))
@ -83,50 +86,71 @@ class TTSEngine:
def split_post(self, text: str, idx: int):
split_files = []
split_text = [
x.group().strip()
for x in re.finditer(
r" *(((.|\n){0," + str(self.tts_module.max_chars) + "})(\.|.$))", text
)
x.group().strip() for x in re.finditer(r" *(((.|\n){0," + str(self.tts_module.max_chars) + "})(\.|.$))", text)
]
offset = 0
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()
self.create_silence_mp3()
# for i in range(0, idy + 1):
# print(f"Cleaning up {self.path}/{idx}-{i}.part.mp3")
idy = None
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):
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:
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")
self.last_clip_length = clip.duration
self.length += clip.duration
self.last_clip_length = clip.duration
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:
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):
lang = settings.config["reddit"]["thread"]["post_lang"]

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

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

@ -62,7 +62,13 @@ function install_macos(){
fi
# Install the 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)

@ -1,18 +1,18 @@
#!/usr/bin/env python
import math
import re
from subprocess import Popen
import sys
from os import name
from subprocess import Popen
from pathlib import Path
from prawcore import ResponseException
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.cleanup import cleanup
from utils.console import print_markdown, print_step
from utils.id import id
from utils.version import checkversion
from video_creation.background import (
download_background,
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.voices import save_text_to_mp3
__VERSION__ = "2.4.1"
__VERSION__ = "2.4.2"
print(
"""
@ -36,7 +36,7 @@ print(
)
# Modified by JasonLovesDoggo
print_markdown(
"### Thanks for using this tool! [Feel free to contribute to this project on GitHub!](https://lewismenelaws.com) If you have any questions, feel free to reach out to me on Twitter or submit a GitHub issue. 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__)
@ -75,14 +75,14 @@ def shutdown():
print("Exiting...")
exit()
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()
try:
if config["settings"]["times_to_run"]:
run_many(config["settings"]["times_to_run"])
elif len(config["reddit"]["thread"]["post_id"].split("+")) > 1:
if len(config["reddit"]["thread"]["post_id"].split("+")) > 1:
for index, post_id in enumerate(config["reddit"]["thread"]["post_id"].split("+")):
index += 1
print_step(
@ -90,6 +90,8 @@ if __name__ == "__main__":
)
main(post_id)
Popen("cls" if name == "nt" else "clear", shell=True).wait()
elif config["settings"]["times_to_run"]:
run_many(config["settings"]["times_to_run"])
else:
main()
except KeyboardInterrupt:

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

@ -12,3 +12,5 @@ toml==0.10.2
translators==5.3.1
pyttsx3==2.90
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]
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_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]
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" }
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" }
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
import re
from rich.columns import Columns
from rich.console import Console
from rich.markdown import Markdown
from rich.padding import Padding
from rich.panel import Panel
from rich.text import Text
from rich.columns import Columns
import re
console = Console()
@ -54,11 +55,7 @@ def handle_input(
return default if default is not NotImplemented else ""
if default is not NotImplemented:
console.print(
"[green]"
+ message
+ '\n[blue bold]The default value is "'
+ str(default)
+ '"\nDo you want to use it?(y/n)'
"[green]" + message + '\n[blue bold]The default value is "' + str(default) + '"\nDo you want to use it?(y/n)'
)
if input().casefold().startswith("y"):
return default
@ -71,9 +68,7 @@ def handle_input(
if check_type is not False:
try:
user_input = check_type(user_input)
if (nmin is not None and user_input < nmin) or (
nmax is not None and user_input > nmax
):
if (nmin is not None and user_input < nmin) or (nmax is not None and user_input > nmax):
# FAILSTATE Input out of bounds
console.print("[red]" + oob_error)
continue
@ -89,9 +84,7 @@ def handle_input(
continue
else:
# FAILSTATE Input STRING out of bounds
if (nmin is not None and len(user_input) < nmin) or (
nmax is not None and len(user_input) > nmax
):
if (nmin is not None and len(user_input) < nmin) or (nmax is not None and len(user_input) > nmax):
console.print("[red bold]" + oob_error)
continue
break # SUCCESS Input STRING in bounds
@ -105,16 +98,8 @@ def handle_input(
isinstance(eval(user_input), check_type)
return check_type(user_input)
except:
console.print(
"[red bold]"
+ err_message
+ "\nValid options are: "
+ ", ".join(map(str, options))
+ "."
)
console.print("[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + ".")
continue
if user_input in options:
return user_input
console.print(
"[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "."
)
console.print("[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
from utils.console import print_substep
def id(reddit_obj: dict):
"""
This function takes a reddit object and returns the post id

@ -1,13 +1,12 @@
#!/usr/bin/env python
import toml
from rich.console import Console
import re
from typing import Tuple, Dict
from pathlib import Path
import toml
from rich.console import Console
from utils.console import handle_input
console = Console()
config = dict # autocomplete
@ -35,17 +34,12 @@ def check(value, checks, name):
except:
incorrect = True
if (
not incorrect and "options" in checks and value not in checks["options"]
): # FAILSTATE Value is not one of the options
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)
)
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
@ -85,9 +79,7 @@ def check(value, checks, name):
err_message=get_check_value("input_error", "Incorrect input"),
nmin=get_check_value("nmin", None),
nmax=get_check_value("nmax", None),
oob_error=get_check_value(
"oob_error", "Input out of bounds(Value too high/low/long/short)"
),
oob_error=get_check_value("oob_error", "Input out of bounds(Value too high/low/long/short)"),
options=get_check_value("options", None),
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__":
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 ")
return get_subreddit_undone(
subreddit.top(
time_filter=VALID_TIME_FILTERS[index], limit=(50 if int(index) == 0 else index + 1 * 50)
),
subreddit.top(time_filter=VALID_TIME_FILTERS[index], limit=(50 if int(index) == 0 else index + 1 * 50)),
subreddit,
times_checked=index,
) # all the videos in hot have already been done

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

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

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

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

@ -1,19 +1,32 @@
from pathlib import Path
import json
import random
from random import randrange
import re
from pathlib import Path
from random import randrange
from typing import Any, Tuple
from moviepy.editor import VideoFileClip
from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
from pytube import YouTube
from pytube.cli import on_progress
from utils import settings
from utils.CONSTANTS import background_options
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]:
"""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
if Path(f"assets/backgrounds/{credit}-{filename}").is_file():
return
print_step(
"We need to download the backgrounds videos. they are fairly large but it's only done once. 😎"
)
print_step("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(f"Downloading {filename} from {uri}")
YouTube(uri, on_progress_callback=on_progress).streams.filter(res="1080p").first().download(

@ -4,6 +4,7 @@ import os
import re
from os.path import exists
from typing import Tuple, Any
from moviepy.audio.AudioClip import concatenate_audioclips, CompositeAudioClip
from moviepy.audio.io.AudioFileClip import AudioFileClip
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 rich.console import Console
from utils import settings
from utils.cleanup import cleanup
from utils.console import print_step, print_substep
from utils.video import Video
from utils.videos import save_data
from utils import settings
console = Console()
W, H = 1080, 1920
@ -118,9 +119,7 @@ def make_final_video(
# )
# else: story mode stuff
img_clip_pos = background_config[3]
image_concat = concatenate_videoclips(image_clips).set_position(
img_clip_pos
) # note transition kwarg for delay in imgs
image_concat = concatenate_videoclips(image_clips).set_position(img_clip_pos) # note transition kwarg for delay in imgs
image_concat.audio = audio_composite
final = CompositeVideoClip([background_clip, image_concat])
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
# # VOLUME_MULTIPLIER) # lower volume by background_audio_volume, use with fx
# final.set_audio(final_audio)
final = Video(final).add_watermark(
text=f"Background credit: {background_config[2]}", opacity=0.4, redditid=reddit_obj
)
final = Video(final).add_watermark(text=f"Background credit: {background_config[2]}", opacity=0.4, redditid=reddit_obj)
final.write_videofile(
f"assets/temp/{id}/temp.mp4",
fps=30,
@ -163,6 +160,4 @@ def make_final_video(
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: {background_config[2]}'
)
print_step(f'Reddit title: {reddit_obj["thread_title"]} \n Background Credit: {background_config[2]}')

@ -1,19 +1,17 @@
import json
from pathlib import Path
import re
from pathlib import Path
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 rich.progress import track
import translators as ts
from utils import settings
from utils.console import print_step, print_substep
# do not remove the above line
storymode = False
@ -32,7 +30,7 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in
with sync_playwright() as p:
print_substep("Launching Headless Browser...")
browser = p.chromium.launch()
browser = p.chromium.launch(headless=True) # add headless=False for debug
context = browser.new_context()
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
if page.locator('[data-click-id="text"] button').is_visible():
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
# 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)
if storymode:
page.locator('[data-click-id="text"]').screenshot(
path=f"assets/temp/{id}/png/story_content.png"
)
page.locator('[data-click-id="text"]').screenshot(path=f"assets/temp/{id}/png/story_content.png")
else:
for idx, comment in enumerate(
track(reddit_object["comments"], "Downloading screenshots...")
):
for idx, comment in enumerate(track(reddit_object["comments"], "Downloading screenshots...")):
# Stop if we have reached the screenshot_num
if idx >= screenshot_num:
break
@ -105,9 +97,7 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in
[comment_tl, comment["comment_id"]],
)
try:
page.locator(f"#t1_{comment['comment_id']}").screenshot(
path=f"assets/temp/{id}/png/comment_{idx}.png"
)
page.locator(f"#t1_{comment['comment_id']}").screenshot(path=f"assets/temp/{id}/png/comment_{idx}.png")
except TimeoutError:
del reddit_object["comments"]
screenshot_num += 1

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

Loading…
Cancel
Save