diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ec78b1a..0f392ff 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/autoblack.yml b/.github/workflows/fmt.yml similarity index 64% rename from .github/workflows/autoblack.yml rename to .github/workflows/fmt.yml index 8e367fe..c09814a 100644 --- a/.github/workflows/autoblack.yml +++ b/.github/workflows/fmt.yml @@ -3,30 +3,35 @@ # Othewrwise, Black is run and its changes are committed back to the incoming pull request. # https://github.com/cclauss/autoblack -name: autoblack +name: fmt on: push: branches: ["develop"] jobs: - build: + format: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Set up Python 3.7 - uses: actions/setup-python@v1 + - uses: actions/checkout@v4 + - name: Set up Python 3.10 + uses: actions/setup-python@v5 with: - python-version: 3.9 - - name: Install Black - run: pip install black - - name: Run black --check . - run: black --check . - - name: If needed, commit black changes to the pull request + python-version: 3.10.14 + - name: Install Black & isort + run: pip install black isort + - name: Run black check + run: black --check . --line-length 101 + - name: Run isort check + run: isort . --check-only --diff --profile black + - name: If needed, commit changes to the pull request if: failure() run: | black . --line-length 101 + isort . --profile black git config --global user.name github-actions git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY git checkout $GITHUB_HEAD_REF git commit -am "fixup: Format Python code with Black" git push origin HEAD:develop + + diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f95531f..e93afee 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,10 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: psf/black@stable with: options: "--line-length 101" + - uses: isort/isort-action@v1 + with: + configuration: "--check-only --diff --profile black" diff --git a/.gitignore b/.gitignore index 41bdd5e..cc6bd18 100644 --- a/.gitignore +++ b/.gitignore @@ -231,7 +231,8 @@ fabric.properties # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser -assets/ +assets/temp +assets/backgrounds /.vscode out .DS_Store @@ -244,4 +245,4 @@ video_creation/data/videos.json video_creation/data/envvars.txt config.toml -*.exe \ No newline at end of file +*.exe diff --git a/Dockerfile b/Dockerfile index 4cf2a71..3f53ada 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10.9-slim +FROM python:3.10.14-slim RUN apt update RUN apt-get install -y ffmpeg @@ -9,8 +9,4 @@ ADD . /app WORKDIR /app RUN pip install -r requirements.txt -# tricks for pytube : https://github.com/elebumm/RedditVideoMakerBot/issues/142 -# (NOTE : This is no longer useful since pytube was removed from the dependencies) -# RUN sed -i 's/re.compile(r"^\\w+\\W")/re.compile(r"^\\$*\\w+\\W")/' /usr/local/lib/python3.8/dist-packages/pytube/cipher.py - CMD ["python3", "main.py"] diff --git a/README.md b/README.md index 5fadd83..e4b7973 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ On macOS and Linux (debian, arch, fedora and centos, and based on those), you ca This can also be used to update the installation 4. Run `python main.py` -5. Visit [the Reddit Apps page.](https://www.reddit.com/prefs/apps), and set up an app that is a "script". Paste any URL in redirect URL. Ex:google.com +5. Visit [the Reddit Apps page.](https://www.reddit.com/prefs/apps), and set up an app that is a "script". Paste any URL in redirect URL. Ex:`https://jasoncameron.dev` 6. The bot will ask you to fill in your details to connect to the Reddit API, and configure the bot to your liking 7. Enjoy 😎 8. If you need to reconfigure the bot, simply open the `config.toml` file and delete the lines that need to be changed. On the next run of the bot, it will help you reconfigure those options. @@ -81,7 +81,7 @@ 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.gg/Vkanmh6C8V) server +### For any questions or support join the [Discord](https://discord.gg/qfQSx45xCV) server ## Developers and maintainers. @@ -101,6 +101,8 @@ Freebiell (Freebie#3263) - https://github.com/FreebieII Aman Raza (electro199#8130) - https://github.com/electro199 +Cyteon (cyteon) - https://github.com/cyteon + ## 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 29542e2..23d2918 100644 --- a/TTS/TikTok.py +++ b/TTS/TikTok.py @@ -2,7 +2,7 @@ import base64 import random import time -from typing import Optional, Final +from typing import Final, Optional import requests diff --git a/TTS/elevenlabs.py b/TTS/elevenlabs.py index e18bba9..e896621 100644 --- a/TTS/elevenlabs.py +++ b/TTS/elevenlabs.py @@ -1,33 +1,28 @@ import random -from elevenlabs import generate, save +from elevenlabs import save +from elevenlabs.client import ElevenLabs from utils import settings -voices = [ - "Adam", - "Antoni", - "Arnold", - "Bella", - "Domi", - "Elli", - "Josh", - "Rachel", - "Sam", -] - class elevenlabs: def __init__(self): self.max_chars = 2500 - self.voices = voices + self.client: ElevenLabs = None def run(self, text, filepath, random_voice: bool = False): + if self.client is None: + self.initialize() if random_voice: voice = self.randomvoice() else: voice = str(settings.config["settings"]["tts"]["elevenlabs_voice_name"]).capitalize() + audio = self.client.generate(text=text, voice=voice, model="eleven_multilingual_v1") + save(audio=audio, filename=filepath) + + def initialize(self): if settings.config["settings"]["tts"]["elevenlabs_api_key"]: api_key = settings.config["settings"]["tts"]["elevenlabs_api_key"] else: @@ -35,8 +30,9 @@ class elevenlabs: "You didn't set an Elevenlabs API key! Please set the config variable ELEVENLABS_API_KEY to a valid API key." ) - audio = generate(api_key=api_key, text=text, voice=voice, model="eleven_multilingual_v1") - save(audio=audio, filename=filepath) + self.client = ElevenLabs(api_key=api_key) def randomvoice(self): - return random.choice(self.voices) + if self.client is None: + self.initialize() + return random.choice(self.client.voices.get_all().voices).voice_name diff --git a/TTS/streamlabs_polly.py b/TTS/streamlabs_polly.py index 3f0610d..1541fac 100644 --- a/TTS/streamlabs_polly.py +++ b/TTS/streamlabs_polly.py @@ -43,9 +43,11 @@ class StreamlabsPolly: 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"} headers = {"Referer": "https://streamlabs.com/"} response = requests.post(self.url, headers=headers, data=body) + if not check_ratelimit(response): self.run(text, filepath, random_voice) diff --git a/assets/title_template.png b/assets/title_template.png new file mode 100644 index 0000000..d726d4f Binary files /dev/null and b/assets/title_template.png differ diff --git a/main.py b/main.py index 85cb930..df2cab0 100755 --- a/main.py +++ b/main.py @@ -9,8 +9,7 @@ from prawcore import ResponseException from reddit.subreddit import get_subreddit_threads from utils import settings from utils.cleanup import cleanup -from utils.console import print_markdown, print_step -from utils.console import print_substep +from utils.console import print_markdown, print_step, print_substep from utils.ffmpeg_install import ffmpeg_install from utils.id import id from utils.version import checkversion @@ -19,16 +18,16 @@ import argparse #!/usr/bin/env python from video_creation.background import ( - download_background_video, - download_background_audio, chop_background, + download_background_audio, + download_background_video, get_background_config, ) 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__ = "3.2.1" +__VERSION__ = "3.3.0" print( """ @@ -40,7 +39,6 @@ print( ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ """ ) -# Modified by JasonLovesDoggo print_markdown( "### Thanks for using this tool! Feel free to contribute to this project on GitHub! If you have any questions, feel free to join my Discord server or submit a GitHub issue. You can find solutions to many common problems in the documentation: https://reddit-video-maker-bot.netlify.app/" ) @@ -88,7 +86,6 @@ if __name__ == "__main__": parser.add_argument("--run-many", type=int, help="Run the program multiple times") parser.add_argument("--id") args = parser.parse_args() - if sys.version_info.major != 3 or sys.version_info.minor not in [10, 11]: print( "Hey! Congratulations, you've made it so far (which is pretty rare with no Python 3.10). Unfortunately, this program only works on Python 3.10. Please install Python 3.10 and try again." diff --git a/reddit/subreddit.py b/reddit/subreddit.py index 8646b48..5f2ac5f 100644 --- a/reddit/subreddit.py +++ b/reddit/subreddit.py @@ -104,7 +104,7 @@ def get_subreddit_threads(POST_ID: str): upvotes = submission.score ratio = submission.upvote_ratio * 100 num_comments = submission.num_comments - threadurl = f"https://reddit.com{submission.permalink}" + threadurl = f"https://new.reddit.com/{submission.permalink}" print_substep(f"Video will be: {submission.title} :thumbsup:", style="bold green") print_substep(f"Thread url is: {threadurl} :thumbsup:", style="bold green") diff --git a/requirements.txt b/requirements.txt index 80208a5..e6e2e7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,23 +1,24 @@ -boto3==1.26.142 -botocore==1.29.142 +boto3==1.34.127 +botocore==1.34.127 gTTS==2.5.1 moviepy==1.0.3 -playwright==1.34.0 -praw==7.7.0 +playwright==1.44.0 +praw==7.7.1 prawcore~=2.3.0 -requests==2.31.0 -rich==13.4.1 +requests==2.32.3 +rich==13.7.1 toml==0.10.2 -translators==5.9.1 +translators==5.9.2 pyttsx3==2.90 Pillow==10.3.0 -tomlkit==0.11.8 -Flask==2.3.3 +tomlkit==0.12.5 +Flask==3.0.3 clean-text==0.6.0 unidecode==1.3.8 -spacy==3.5.3 -torch==2.3.0 -transformers==4.40.2 +spacy==3.7.5 +torch==2.3.1 +transformers==4.41.2 ffmpeg-python==0.2.0 -elevenlabs==0.2.17 -yt-dlp==2023.7.6 \ No newline at end of file +elevenlabs==1.3.0 +yt-dlp==2024.5.27 +numpy==1.26.4 diff --git a/utils/.config.template.toml b/utils/.config.template.toml index e132ea8..6586909 100644 --- a/utils/.config.template.toml +++ b/utils/.config.template.toml @@ -31,6 +31,7 @@ storymode_max_length = { optional = true, default = 1000, example = 1000, explan resolution_w = { optional = false, default = 1080, example = 1440, explantation = "Sets the width in pixels of the final video" } resolution_h = { optional = false, default = 1920, example = 2560, explantation = "Sets the height in pixels of the final video" } zoom = { optional = true, default = 1, example = 1.1, explanation = "Sets the browser zoom level. Useful if you want the text larger.", type = "float", nmin = 0.1, nmax = 2, oob_error = "The text is really difficult to read at a zoom level higher than 2" } +channel_name = { optional = true, default = "Reddit Tales", example = "Reddit Stories", explanation = "Sets the channel name for the video" } [settings.background] background_video = { optional = true, default = "minecraft", example = "rocket-league", options = ["minecraft", "gta", "rocket-league", "motor-gta", "csgo-surf", "cluster-truck", "minecraft-2","multiversus","fall-guys","steep", ""], explanation = "Sets the background for the video based on game name" } diff --git a/utils/ai_methods.py b/utils/ai_methods.py index eb6e73e..e628942 100644 --- a/utils/ai_methods.py +++ b/utils/ai_methods.py @@ -1,6 +1,6 @@ import numpy as np import torch -from transformers import AutoTokenizer, AutoModel +from transformers import AutoModel, AutoTokenizer # Mean Pooling - Take attention mask into account for correct averaging diff --git a/utils/fonts.py b/utils/fonts.py new file mode 100644 index 0000000..4980f6a --- /dev/null +++ b/utils/fonts.py @@ -0,0 +1,13 @@ +from PIL.ImageFont import FreeTypeFont, ImageFont + + +def getsize(font: ImageFont | FreeTypeFont, text: str): + left, top, right, bottom = font.getbbox(text) + width = right - left + height = bottom - top + return width, height + + +def getheight(font: ImageFont | FreeTypeFont, text: str): + _, height = getsize(font, text) + return height diff --git a/utils/imagenarator.py b/utils/imagenarator.py index 151b0e6..509882d 100644 --- a/utils/imagenarator.py +++ b/utils/imagenarator.py @@ -6,6 +6,7 @@ from PIL import Image, ImageDraw, ImageFont from rich.progress import track from TTS.engine_wrapper import process_text +from utils.fonts import getheight, getsize def draw_multiple_line_text( @@ -15,12 +16,12 @@ def draw_multiple_line_text( Draw multiline text over given image """ draw = ImageDraw.Draw(image) - Fontperm = font.getsize(text) + font_height = getheight(font, text) image_width, image_height = image.size lines = textwrap.wrap(text, width=wrap) - y = (image_height / 2) - (((Fontperm[1] + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + y = (image_height / 2) - (((font_height + (len(lines) * padding) / len(lines)) * len(lines)) / 2) for line in lines: - line_width, line_height = font.getsize(line) + line_width, line_height = getsize(font, line) if transparent: shadowcolor = "black" for i in range(1, 5): @@ -56,25 +57,17 @@ def imagemaker(theme, reddit_obj: dict, txtclr, padding=5, transparent=False) -> """ Render Images for video """ - title = process_text(reddit_obj["thread_title"], False) texts = reddit_obj["thread_post"] id = re.sub(r"[^\w\s-]", "", reddit_obj["thread_id"]) if transparent: font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) - tfont = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) else: - tfont = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 100) # for title font = ImageFont.truetype(os.path.join("fonts", "Roboto-Regular.ttf"), 100) size = (1920, 1080) image = Image.new("RGBA", size, theme) - # for title - draw_multiple_line_text(image, title, tfont, txtclr, padding, wrap=30, transparent=transparent) - - image.save(f"assets/temp/{id}/png/title.png") - for idx, text in track(enumerate(texts), "Rendering Image"): image = Image.new("RGBA", size, theme) text = process_text(text, False) diff --git a/utils/settings.py b/utils/settings.py index 8187e9a..2ebaef3 100755 --- a/utils/settings.py +++ b/utils/settings.py @@ -1,6 +1,6 @@ import re from pathlib import Path -from typing import Tuple, Dict +from typing import Dict, Tuple import toml from rich.console import Console diff --git a/video_creation/background.py b/video_creation/background.py index 2ec9812..43be69a 100644 --- a/video_creation/background.py +++ b/video_creation/background.py @@ -3,10 +3,10 @@ import random import re from pathlib import Path from random import randrange -from typing import Any, Tuple, Dict +from typing import Any, Dict, Tuple import yt_dlp -from moviepy.editor import VideoFileClip, AudioFileClip +from moviepy.editor import AudioFileClip, VideoFileClip from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip from utils import settings diff --git a/video_creation/data/videos.json b/video_creation/data/videos.json deleted file mode 100644 index fe51488..0000000 --- a/video_creation/data/videos.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/video_creation/final_video.py b/video_creation/final_video.py index 5069474..101d0f7 100644 --- a/video_creation/final_video.py +++ b/video_creation/final_video.py @@ -2,21 +2,23 @@ import multiprocessing import os import re import tempfile +import textwrap import threading import time from os.path import exists # Needs to be imported specifically -from typing import Final -from typing import Tuple, Dict +from pathlib import Path +from typing import Dict, Final, Tuple import ffmpeg import translators -from PIL import Image +from PIL import Image, ImageDraw, ImageFont from rich.console import Console from rich.progress import track from utils import settings from utils.cleanup import cleanup from utils.console import print_step, print_substep +from utils.fonts import getheight from utils.thumbnail import create_thumbnail from utils.videos import save_data @@ -106,6 +108,63 @@ def prepare_background(reddit_id: str, W: int, H: int) -> str: return output_path +def create_fancy_thumbnail(image, text, text_color, padding, wrap=35): + print_step(f"Creating fancy thumbnail for: {text}") + font_title_size = 47 + font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) + image_width, image_height = image.size + lines = textwrap.wrap(text, width=wrap) + y = ( + (image_height / 2) + - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + + 30 + ) + draw = ImageDraw.Draw(image) + + username_font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), 30) + draw.text( + (205, 825), + settings.config["settings"]["channel_name"], + font=username_font, + fill=text_color, + align="left", + ) + + if len(lines) == 3: + lines = textwrap.wrap(text, width=wrap + 10) + font_title_size = 40 + font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) + y = ( + (image_height / 2) + - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + + 35 + ) + elif len(lines) == 4: + lines = textwrap.wrap(text, width=wrap + 10) + font_title_size = 35 + font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) + y = ( + (image_height / 2) + - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + + 40 + ) + elif len(lines) > 4: + lines = textwrap.wrap(text, width=wrap + 10) + font_title_size = 30 + font = ImageFont.truetype(os.path.join("fonts", "Roboto-Bold.ttf"), font_title_size) + y = ( + (image_height / 2) + - (((getheight(font, text) + (len(lines) * padding) / len(lines)) * len(lines)) / 2) + + 30 + ) + + for line in lines: + draw.text((120, y), line, font=font, fill=text_color, align="left") + y += getheight(font, line) + padding + + return image + + def merge_background_audio(audio: ffmpeg, reddit_id: str): """Gather an audio and merge with assets/backgrounds/background.mp3 Args: @@ -201,6 +260,23 @@ def make_final_video( image_clips = list() + Path(f"assets/temp/{reddit_id}/png").mkdir(parents=True, exist_ok=True) + + # Credits to tim (beingbored) + # get the title_template image and draw a text in the middle part of it with the title of the thread + title_template = Image.open("assets/title_template.png") + + title = reddit_obj["thread_title"] + + title = name_normalize(title) + + font_color = "#000000" + padding = 5 + + # create_fancy_thumbnail(image, text, text_color, padding + title_img = create_fancy_thumbnail(title_template, title, font_color, padding) + + title_img.save(f"assets/temp/{reddit_id}/png/title.png") image_clips.insert( 0, ffmpeg.input(f"assets/temp/{reddit_id}/png/title.png")["v"].filter( @@ -256,6 +332,9 @@ def make_final_video( ) ) image_overlay = image_clips[i].filter("colorchannelmixer", aa=opacity) + assert ( + audio_clips_durations is not None + ), "Please make a GitHub issue if you see this. Ping @JasonLovesDoggo on GitHub." background_clip = background_clip.overlay( image_overlay, enable=f"between(t,{current_time},{current_time + audio_clips_durations[i]})", diff --git a/video_creation/screenshot_downloader.py b/video_creation/screenshot_downloader.py index cdcf8ef..6b56e99 100644 --- a/video_creation/screenshot_downloader.py +++ b/video_creation/screenshot_downloader.py @@ -13,7 +13,7 @@ from utils.imagenarator import imagemaker from utils.playwright import clear_cookie_by_name from utils.videos import save_data -__all__ = ["download_screenshots_of_reddit_posts"] +__all__ = ["get_screenshots_of_reddit_posts"] def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): @@ -58,6 +58,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): bgcolor = (255, 255, 255, 255) txtcolor = (0, 0, 0) transparent = False + if storymode and settings.config["settings"]["storymodemethod"] == 1: # for idx,item in enumerate(reddit_object["thread_post"]): print_substep("Generating images...") @@ -85,6 +86,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): color_scheme="dark", viewport=ViewportSize(width=W, height=H), device_scale_factor=dsf, + user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", ) cookies = json.load(cookie_file) cookie_file.close() @@ -98,9 +100,9 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): 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[class$='m-full-width']").click() + page.locator(f'input[name="username"]').fill(settings.config["reddit"]["creds"]["username"]) + page.locator(f'input[name="password"]').fill(settings.config["reddit"]["creds"]["password"]) + page.get_by_role("button", name="Log In").click() page.wait_for_timeout(5000) login_error_div = page.locator(".AnimatedForm__errorMessage").first @@ -218,7 +220,7 @@ def get_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int): if page.locator('[data-testid="content-gate"]').is_visible(): page.locator('[data-testid="content-gate"] button').click() - page.goto(f'https://reddit.com{comment["comment_url"]}', timeout=0) + page.goto(f"https://new.reddit.com/{comment['comment_url']}") # translate code diff --git a/video_creation/voices.py b/video_creation/voices.py index 8495f8d..ad94a14 100644 --- a/video_creation/voices.py +++ b/video_creation/voices.py @@ -2,15 +2,15 @@ from typing import Tuple from rich.console import Console -from TTS.GTTS import GTTS -from TTS.TikTok import TikTok from TTS.aws_polly import AWSPolly from TTS.elevenlabs import elevenlabs from TTS.engine_wrapper import TTSEngine +from TTS.GTTS import GTTS from TTS.pyttsx import pyttsx from TTS.streamlabs_polly import StreamlabsPolly +from TTS.TikTok import TikTok from utils import settings -from utils.console import print_table, print_step +from utils.console import print_step, print_table console = Console()