diff --git a/GUI/settings.html b/GUI/settings.html
index 9013600..1f0ef2e 100644
--- a/GUI/settings.html
+++ b/GUI/settings.html
@@ -369,6 +369,19 @@
+
diff --git a/README.md b/README.md
index 4997d3f..f381452 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,8 @@ I have tried to simplify the code so anyone can read it and start contributing a
Please read our [contributing guidelines](CONTRIBUTING.md) for more detailed information.
+### For any questions or support join the [Discord](https://discord.com/channels/897666935708352582/) server
+
## Developers and maintainers.
Elebumm (Lewis#6305) - https://github.com/elebumm (Founder)
@@ -96,3 +98,9 @@ Verq (Verq#2338) - https://github.com/CordlessCoder
LukaHietala (Pix.#0001) - https://github.com/LukaHietala
Freebiell (Freebie#3263) - https://github.com/FreebieII
+
+Aman Raza (electro199#8130) - https://github.com/electro199
+
+
+## LICENSE
+[Roboto Fonts](https://fonts.google.com/specimen/Roboto/about) are licensed under [Apache License V2](https://www.apache.org/licenses/LICENSE-2.0)
diff --git a/TTS/TikTok.py b/TTS/TikTok.py
index a0f8993..2bcbd6d 100644
--- a/TTS/TikTok.py
+++ b/TTS/TikTok.py
@@ -1,26 +1,28 @@
+# documentation for tiktok api: https://github.com/oscie57/tiktok-voice/wiki
import base64
import random
+import time
+from typing import Optional, Final
import requests
-from requests.adapters import HTTPAdapter, Retry
from utils import settings
-# from profanity_filter import ProfanityFilter
-# pf = ProfanityFilter()
-# Code by @JasonLovesDoggo
-# https://twitter.com/scanlime/status/1512598559769702406
+__all__ = ["TikTok", "TikTokTTSException"]
-nonhuman = [ # DISNEY VOICES
+disney_voices: Final[tuple] = (
"en_us_ghostface", # Ghost Face
"en_us_chewbacca", # Chewbacca
"en_us_c3po", # C3PO
"en_us_stitch", # Stitch
"en_us_stormtrooper", # Stormtrooper
"en_us_rocket", # Rocket
- # ENGLISH VOICES
-]
-human = [
+ "en_female_madam_leota", # Madame Leota
+ "en_male_ghosthost", # Ghost Host
+ "en_male_pirate", # pirate
+)
+
+eng_voices: Final[tuple] = (
"en_au_001", # English AU - Female
"en_au_002", # English AU - Male
"en_uk_001", # English UK - Male 1
@@ -30,23 +32,28 @@ human = [
"en_us_006", # English US - Male 1
"en_us_007", # English US - Male 2
"en_us_009", # English US - Male 3
- "en_us_010",
-]
-voices = nonhuman + human
+ "en_us_010", # English US - Male 4
+ "en_male_narration", # Narrator
+ "en_male_funny", # Funny
+ "en_female_emotional", # Peaceful
+ "en_male_cody", # Serious
+)
-noneng = [
+non_eng_voices: Final[tuple] = (
+ # Western European voices
"fr_001", # French - Male 1
"fr_002", # French - Male 2
"de_001", # German - Female
"de_002", # German - Male
"es_002", # Spanish - Male
- # AMERICA VOICES
+ "it_male_m18" # Italian - Male
+ # South american voices
"es_mx_002", # Spanish MX - Male
"br_001", # Portuguese BR - Female 1
"br_003", # Portuguese BR - Female 2
"br_004", # Portuguese BR - Female 3
"br_005", # Portuguese BR - Male
- # ASIA VOICES
+ # asian voices
"id_001", # Indonesian - Female
"jp_001", # Japanese - Female 1
"jp_003", # Japanese - Female 2
@@ -55,51 +62,106 @@ noneng = [
"kr_002", # Korean - Male 1
"kr_003", # Korean - Female
"kr_004", # Korean - Male 2
-]
-
+)
-# good_voices = {'good': ['en_us_002', 'en_us_006'],
-# 'ok': ['en_au_002', 'en_uk_001']} # less en_us_stormtrooper more less en_us_rocket en_us_ghostface
+vocals: Final[tuple] = (
+ "en_female_f08_salut_damour", # Alto
+ "en_male_m03_lobby", # Tenor
+ "en_male_m03_sunshine_soon", # Sunshine Soon
+ "en_female_f08_warmy_breeze", # Warmy Breeze
+ "en_female_ht_f08_glorious", # Glorious
+ "en_male_sing_funny_it_goes_up", # It Goes Up
+ "en_male_m2_xhxs_m03_silly", # Chipmunk
+ "en_female_ht_f08_wonderful_world", # Dramatic
+)
-class TikTok: # TikTok Text-to-Speech Wrapper
+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="
+ if not settings.config['settings']['tts']['tiktok_sessionid']:
+ raise TikTokTTSException(5)
+ headers = {
+ "User-Agent": "com.zhiliaoapp.musically/2022600030 (Linux; U; Android 7.1.2; es_ES; SM-G988N; "
+ "Build/NRD90M;tt-ok/3.12.13.1)",
+ "Cookie": f"sessionid={settings.config['settings']['tts']['tiktok_sessionid']}",
+ }
+
+ self.URI_BASE = "https://api16-normal-c-useast1a.tiktokv.com/media/api/text/speech/invoke/"
self.max_chars = 300
- self.voices = {"human": human, "nonhuman": nonhuman, "noneng": noneng}
-
- def run(self, text, filepath, random_voice: bool = False):
- # if censor:
- # req_text = pf.censor(req_text)
- # pass
- voice = (
- self.randomvoice()
- if random_voice
- else (
- settings.config["settings"]["tts"]["tiktok_voice"]
- or random.choice(self.voices["human"])
- )
- )
+
+ self._session = requests.Session()
+ # set the headers to the session, so we don't have to do it for every request
+ self._session.headers = headers
+
+ def run(self, text: str, filepath: str, random_voice: bool = False):
+ if random_voice:
+ voice = self.random_voice()
+ else:
+ # if tiktok_voice is not set in the config file, then use a random voice
+ voice = settings.config["settings"]["tts"].get("tiktok_voice", None)
+
+ # get the audio from the TikTok API
+ data = self.get_voices(voice=voice, text=text)
+
+ # check if there was an error in the request
+ status_code = data["status_code"]
+ if status_code != 0:
+ raise TikTokTTSException(status_code, data["message"])
+
+ # decode data from base64 to binary
try:
- r = requests.post(
- f"{self.URI_BASE}{voice}&req_text={text}&speaker_map_type=0"
- )
- except requests.exceptions.SSLError:
- # https://stackoverflow.com/a/47475019/18516611
- session = requests.Session()
- retry = Retry(connect=3, backoff_factor=0.5)
- adapter = HTTPAdapter(max_retries=retry)
- session.mount("http://", adapter)
- session.mount("https://", adapter)
- r = session.post(
- f"{self.URI_BASE}{voice}&req_text={text}&speaker_map_type=0"
- )
- # print(r.text)
- vstr = [r.json()["data"]["v_str"]][0]
- b64d = base64.b64decode(vstr)
+ raw_voices = data["data"]["v_str"]
+ except:
+ print("The TikTok TTS returned an invalid response. Please try again later, and report this bug.")
+ raise TikTokTTSException(0, "Invalid response")
+ decoded_voices = base64.b64decode(raw_voices)
+ # write voices to specified filepath
with open(filepath, "wb") as out:
- out.write(b64d)
+ out.write(decoded_voices)
+
+ def get_voices(self, text: str, voice: Optional[str] = None) -> dict:
+ """If voice is not passed, the API will try to use the most fitting voice"""
+ # sanitize text
+ text = text.replace("+", "plus").replace("&", "and").replace("r/", "")
+
+ # prepare url request
+ params = {"req_text": text, "speaker_map_type": 0, "aid": 1233}
+
+ if voice is not None:
+ params["text_speaker"] = voice
+
+ # send request
+ try:
+ response = self._session.post(self.URI_BASE, params=params)
+ except ConnectionError:
+ time.sleep(random.randrange(1, 7))
+ response = self._session.post(self.URI_BASE, params=params)
+
+ return response.json()
+
+ @staticmethod
+ def random_voice() -> str:
+ return random.choice(eng_voices)
+
+
+class TikTokTTSException(Exception):
+ def __init__(self, code: int, message: str):
+ self._code = code
+ self._message = message
+
+ def __str__(self) -> str:
+ if self._code == 1:
+ return f"Code: {self._code}, reason: probably the aid value isn't correct, message: {self._message}"
+
+ if self._code == 2:
+ return f"Code: {self._code}, reason: the text is too long, message: {self._message}"
+
+ if self._code == 4:
+ return f"Code: {self._code}, reason: the speaker doesn't exist, message: {self._message}"
+
+ if self._code == 5:
+ return f"You have to add session id in config to use titok TTS"
- def randomvoice(self):
- return random.choice(self.voices["human"])
+ return f"Code: {self._message}, reason: unknown, message: {self._message}"
diff --git a/main.py b/main.py
index bb479d5..7e4d1b9 100755
--- a/main.py
+++ b/main.py
@@ -23,7 +23,7 @@ from video_creation.final_video import make_final_video
from video_creation.screenshot_downloader import get_screenshots_of_reddit_posts
from video_creation.voices import save_text_to_mp3
-__VERSION__ = "2.5.0"
+__VERSION__ = "3.0.1"
print(
"""
@@ -109,7 +109,7 @@ if __name__ == "__main__":
shutdown()
except Exception as err:
print_step(f'''
- Sorry, something went wrong with this test version! Try again, and feel free to report this issue at GitHub or the Discord community.\n
+ Sorry, something went wrong with this version! Try again, and feel free to report this issue at GitHub or the Discord community.\n
Version: {__VERSION__} \n
Story mode: {str(config["settings"]["storymode"])} \n
Story mode method: {str(config["settings"]["storymodemethod"])}
diff --git a/reddit/subreddit.py b/reddit/subreddit.py
index 8fb9e9d..ed1e8cf 100644
--- a/reddit/subreddit.py
+++ b/reddit/subreddit.py
@@ -134,6 +134,7 @@ def get_subreddit_threads(POST_ID: str):
content["thread_url"] = threadurl
content["thread_title"] = submission.title
content["thread_id"] = submission.id
+ content["is_nsfw"] = submission.over_18
content["comments"] = []
if settings.config["settings"]["storymode"]:
if settings.config["settings"]["storymodemethod"] == 1:
diff --git a/requirements.txt b/requirements.txt
index 77a2264..22d3d7f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,11 +7,11 @@ praw==7.6.1
prawcore~=2.3.0
pytube==12.1.0
requests==2.28.1
-rich==12.5.1
+rich==13.3.1
toml==0.10.2
translators==5.3.1
pyttsx3==2.90
-Pillow~=9.3.0
+Pillow~=9.4.0
tomlkit==0.11.4
Flask==2.2.2
clean-text==0.6.0
diff --git a/utils/.config.template.toml b/utils/.config.template.toml
index 25b5797..ec5bb2a 100644
--- a/utils/.config.template.toml
+++ b/utils/.config.template.toml
@@ -43,11 +43,12 @@ background_thumbnail_font_size = { optional = true, type = "int", default = 96,
background_thumbnail_font_color = { optional = true, default = "255,255,255", example = "255,255,255", explanation = "Font color in RGB format for the thumbnail text" }
[settings.tts]
-voice_choice = { optional = false, default = "googletranslate", 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 = true, default = "Matthew", example = "Matthew", explanation = "The voice used for AWS Polly" }
-streamlabs_polly_voice = { optional = true, default = "Matthew", example = "Matthew", explanation = "The voice used for Streamlabs Polly" }
-tiktok_voice = { optional = true, default = "en_us_006", example = "en_us_006", explanation = "The voice used for TikTok TTS" }
-python_voice = { optional = true, 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 = true, default = "2", example = "2", explanation = "The number of system voices (2 are pre-installed in Windows)" }
+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 = true, default = "en_us_001", example = "en_us_006", explanation = "The voice used for TikTok TTS" }
+tiktok_sessionid = { optional = true, example = "c76bcc3a7625abcc27b508c7db457ff1", explanation = "TikTok sessionid needed for the TTS API request. Check documentation if you don't know how to obtain it." }
+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)" }
silence_duration = { optional = true, example = "0.1", explanation = "Time in seconds between TTS comments", default = 0.3, type = "float" }
no_emojis = { optional = false, type = "bool", default = false, example = false, options = [true, false,], explanation = "Whether to remove emojis from the comments" }
\ No newline at end of file
diff --git a/utils/backgrounds.json b/utils/backgrounds.json
index 8cb01d1..6e00992 100644
--- a/utils/backgrounds.json
+++ b/utils/backgrounds.json
@@ -4,13 +4,13 @@
"https://www.youtube.com/watch?v=vw5L4xCPy9Q",
"bike-parkour-gta.mp4",
"Achy Gaming",
- 480
+ "center"
],
"rocket-league": [
"https://www.youtube.com/watch?v=2X9QGY__0II",
"rocket_league.mp4",
"Orbital Gameplay",
- 200
+ "center"
],
"minecraft": [
"https://www.youtube.com/watch?v=n_Dv4JMiwK8",
@@ -22,7 +22,7 @@
"https://www.youtube.com/watch?v=qGa9kWREOnE",
"gta-stunt-race.mp4",
"Achy Gaming",
- 480
+ "center"
],
"csgo-surf": [
"https://www.youtube.com/watch?v=E-8JlyO59Io",
@@ -34,7 +34,7 @@
"https://www.youtube.com/watch?v=uVKxtdMgJVU",
"cluster_truck.mp4",
"No Copyright Gameplay",
- 480
+ "center"
],
"minecraft-2": [
"https://www.youtube.com/watch?v=Pt5_GSKIWQM",
diff --git a/utils/gui_utils.py b/utils/gui_utils.py
index b539ff7..9d644d8 100644
--- a/utils/gui_utils.py
+++ b/utils/gui_utils.py
@@ -162,7 +162,7 @@ def delete_background(key):
# 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(
+ regex = re.compile(r"(?:\/|%3D|v=|vi=)([0-9A-z\-_]{11})(?:[%#?&]|$)").search(
youtube_uri
)
diff --git a/utils/imagenarator.py b/utils/imagenarator.py
index ac53d82..8e3789e 100644
--- a/utils/imagenarator.py
+++ b/utils/imagenarator.py
@@ -1,11 +1,12 @@
import re
import textwrap
+import os
from PIL import Image, ImageDraw, ImageFont
from rich.progress import track
from TTS.engine_wrapper import process_text
-def draw_multiple_line_text(image, text, font, text_color, padding, wrap=50):
+def draw_multiple_line_text(image, text, font, text_color, padding, wrap=50) -> None:
"""
Draw multiline text over given image
"""
@@ -23,7 +24,7 @@ def draw_multiple_line_text(image, text, font, text_color, padding, wrap=50):
# theme=bgcolor,reddit_obj=reddit_object,txtclr=txtcolor
-def imagemaker(theme, reddit_obj: dict, txtclr, padding=5):
+def imagemaker(theme, reddit_obj: dict, txtclr, padding=5) -> None:
"""
Render Images for video
"""
@@ -31,9 +32,9 @@ def imagemaker(theme, reddit_obj: dict, txtclr, padding=5):
texts = reddit_obj["thread_post"]
id = re.sub(r"[^\w\s-]", "", reddit_obj["thread_id"])
- tfont = ImageFont.truetype("fonts\\Roboto-Bold.ttf", 27) # for title
+ tfont = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 27) # for title
font = ImageFont.truetype(
- "fonts\\Roboto-Regular.ttf", 20
+ os.path.join("fonts", "Roboto-Regular.ttf"), 20
) # for despcription|comments
size = (500, 176)
diff --git a/utils/voice.py b/utils/voice.py
index a88c87d..76efc20 100644
--- a/utils/voice.py
+++ b/utils/voice.py
@@ -13,7 +13,7 @@ if sys.version_info[0] >= 3:
from datetime import timezone
-def check_ratelimit(response: Response):
+def check_ratelimit(response: Response) -> bool:
"""
Checks if the response is a ratelimit response.
If it is, it sleeps for the time specified in the response.
@@ -30,7 +30,7 @@ def check_ratelimit(response: Response):
return True
-def sleep_until(time):
+def sleep_until(time) -> None:
"""
Pause your program until a specific end time.
'time' is either a valid datetime object or unix timestamp in seconds (i.e. seconds since Unix epoch)
diff --git a/video_creation/background.py b/video_creation/background.py
index 0405e66..0458ce6 100644
--- a/video_creation/background.py
+++ b/video_creation/background.py
@@ -13,7 +13,7 @@ from utils import settings
from utils.console import print_step, print_substep
# Load background videos
-with open("utils/backgrounds.json") as json_file:
+with open("./utils/backgrounds.json") as json_file:
background_options = json.load(json_file)
# Remove "__comment" from backgrounds
diff --git a/video_creation/final_video.py b/video_creation/final_video.py
old mode 100755
new mode 100644
diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py
index 3a76b5b..71bfb70 100644
--- a/video_creation/screenshot_downloader.py
+++ b/video_creation/screenshot_downloader.py
@@ -4,7 +4,6 @@ from pathlib import Path
from typing import Dict, Final
import translators as ts
-from playwright.async_api import async_playwright # pylint: disable=unused-import
from playwright.sync_api import ViewportSize, sync_playwright
from rich.progress import track
@@ -12,9 +11,11 @@ from utils import settings
from utils.console import print_step, print_substep
from utils.imagenarator import imagemaker
+from utils.videos import save_data
__all__ = ["download_screenshots_of_reddit_posts"]
+
def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
"""Downloads screenshots of reddit posts as seen on the web. Downloads to assets/temp/png
@@ -37,7 +38,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
with sync_playwright() as p:
print_substep("Launching Headless Browser...")
- browser = p.chromium.launch() # headless=False #to check for chrome view
+ browser = p.chromium.launch() # headless=False for debugging
context = browser.new_context()
# Device scale factor (or dsf for short) allows us to increase the resolution of the screenshots
# When the dsf is 1, the width of the screenshot is 600 pixels
@@ -71,6 +72,20 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
context.add_cookies(cookies) # load preference cookies
+ # Login to Reddit
+ print_substep("Logging in to Reddit...")
+ page = context.new_page()
+ page.goto("https://www.reddit.com/login", timeout=0)
+ page.set_viewport_size(ViewportSize(width=1920, height=1080))
+ page.wait_for_load_state()
+
+ page.locator('[name="username"]').fill(settings.config["reddit"]["creds"]["username"])
+ page.locator('[name="password"]').fill(settings.config["reddit"]["creds"]["password"])
+ page.locator("button:has-text('Log In')").click()
+
+ page.wait_for_load_state() # Wait for page to fully load and add 5 seconds
+ page.wait_for_timeout(5000)
+
# Get the thread screenshot
page = context.new_page()
page.goto(reddit_object["thread_url"], timeout=0)
@@ -105,7 +120,23 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
print_substep("Skipping translation...")
postcontentpath = f"assets/temp/{reddit_id}/png/title.png"
- page.locator('[data-test-id="post-content"]').screenshot(path=postcontentpath)
+ try:
+ page.locator('[data-test-id="post-content"]').screenshot(path=postcontentpath)
+ except Exception as e:
+ OKGREEN = '\033[92m'
+ WARNING = '\033[93m'
+ ENDC = '\033[0m'
+ print_step(f"{WARNING}Something went wrong!{ENDC}")
+ resp = input("Something went wrong with making the screenshots! Do you want to skip the post? (y/n) ")
+ if resp.casefold().startswith("y"):
+ save_data("", "", "skipped", reddit_id, "")
+ print(f"{OKGREEN}The post is successfully skipped! You can now restart the program and this post will skipped.{ENDC}")
+ resp = input("Do you want the error traceback for debugging purposes? (y/n)")
+ if resp.casefold().startswith("y"):
+ print(e)
+ exit()
+ else:
+ exit()
if storymode:
page.locator('[data-click-id="text"]').first.screenshot(
@@ -151,6 +182,4 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
# close browser instance when we are done using it
browser.close()
-
-
- print_substep("Screenshots downloaded Successfully.", style="bold green")
\ No newline at end of file
+ print_substep("Screenshots downloaded Successfully.", style="bold green")