Commit
·
cf72e88
1
Parent(s):
bfb1bc2
Added src folder
Browse files- pages/__init__.py +0 -0
- pages/reddit.py +123 -0
- src/__init__.py +0 -0
- src/arg_parser.py +98 -0
- src/logger.py +38 -0
- src/subtitle_creator.py +37 -0
- src/text_to_speech.py +8 -0
- src/tiktok.py +30 -0
- src/video_creator.py +95 -0
- src/video_downloader.py +25 -0
- src/video_prepare.py +62 -0
- src/voice_manager.py +17 -0
pages/__init__.py
ADDED
File without changes
|
pages/reddit.py
ADDED
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pathlib import Path
|
2 |
+
import random
|
3 |
+
import streamlit as st
|
4 |
+
|
5 |
+
import praw
|
6 |
+
|
7 |
+
HOME = Path(__name__).parent.absolute()
|
8 |
+
|
9 |
+
|
10 |
+
@st.cache_data
|
11 |
+
def create_instance(*args, **kwargs):
|
12 |
+
reddit = praw.Reddit(
|
13 |
+
client_id=kwargs.get('client_id'),
|
14 |
+
client_secret=kwargs.get('client_secret'),
|
15 |
+
user_agent=kwargs.get('user_agent'),
|
16 |
+
)
|
17 |
+
|
18 |
+
subreddit = get_subreddit(reddit=reddit, subreddit=kwargs.get(
|
19 |
+
'subreddit'), nsfw=kwargs.get('nsfw'))
|
20 |
+
submission = get_random_submission(subreddit=subreddit)
|
21 |
+
st.session_state['submission'] = submission
|
22 |
+
return True
|
23 |
+
|
24 |
+
|
25 |
+
def get_subreddit(*args, **kwargs):
|
26 |
+
reddit = kwargs.get('reddit')
|
27 |
+
subreddit = reddit.subreddit(kwargs.get('subreddit'))
|
28 |
+
nsfw = kwargs.get('nsfw')
|
29 |
+
try:
|
30 |
+
st.text(f"Subreddit: {subreddit.display_name}")
|
31 |
+
except Exception as exception:
|
32 |
+
st.exception(exception=exception)
|
33 |
+
|
34 |
+
if subreddit.over18 and not nsfw:
|
35 |
+
st.error(
|
36 |
+
body='subreddit has NSFW contents but you did not select to scrape them')
|
37 |
+
return subreddit
|
38 |
+
|
39 |
+
|
40 |
+
def get_random_submission(*args, **kwargs):
|
41 |
+
subreddit = kwargs.get('subreddit')
|
42 |
+
submissions = [submission for submission in subreddit.hot(limit=10)]
|
43 |
+
return random.choice(submissions)
|
44 |
+
|
45 |
+
|
46 |
+
# Streamlit Config
|
47 |
+
st.set_page_config(
|
48 |
+
page_title="Whisper-TikTok",
|
49 |
+
page_icon="💬",
|
50 |
+
layout="wide",
|
51 |
+
initial_sidebar_state="expanded",
|
52 |
+
menu_items={
|
53 |
+
'Get Help': 'https://github.com/MatteoFasulo/Whisper-TikTok',
|
54 |
+
'Report a bug': "https://github.com/MatteoFasulo/Whisper-TikTok/issues",
|
55 |
+
'About':
|
56 |
+
"""
|
57 |
+
# Whisper-TikTok
|
58 |
+
Whisper-TikTok is an innovative AI-powered tool that leverages the prowess of Edge TTS, OpenAI-Whisper, and FFMPEG to craft captivating TikTok videos also with a web application interface!
|
59 |
+
|
60 |
+
Mantainer: https://github.com/MatteoFasulo
|
61 |
+
|
62 |
+
If you find a bug or if you just have questions about the project feel free to reach me at https://github.com/MatteoFasulo/Whisper-TikTok
|
63 |
+
Any contribution to this project is welcome to improve the quality of work!
|
64 |
+
"""
|
65 |
+
}
|
66 |
+
)
|
67 |
+
|
68 |
+
st.page_link("app.py", label="Home", icon="🏠")
|
69 |
+
st.page_link("https://github.com/MatteoFasulo/Whisper-TikTok",
|
70 |
+
label="GitHub", icon="🌎")
|
71 |
+
|
72 |
+
with st.sidebar:
|
73 |
+
with st.expander("ℹ️ How to use"):
|
74 |
+
st.write(
|
75 |
+
"""
|
76 |
+
Before starting you will need to create a new [Reddit API App](https://www.reddit.com/prefs/apps) by selecting `script` (personal use).
|
77 |
+
Then, after putting the App name, http://localhost as `reddit uri` and `about url`, you have just to insert those values in this dashboard to use the Reddit API for scraping any subreddit.
|
78 |
+
""")
|
79 |
+
client_id = st.text_input(label='Reddit Client ID')
|
80 |
+
client_secret = st.text_input(
|
81 |
+
label='Reddit Client Secret', type='password')
|
82 |
+
user_agent = st.text_input(label='Reddit User Agent')
|
83 |
+
|
84 |
+
|
85 |
+
st.title("🏆 Whisper-TikTok 🚀")
|
86 |
+
st.subheader('Reddit section')
|
87 |
+
st.write("""
|
88 |
+
This section allows you to generate videos from subreddits.""")
|
89 |
+
|
90 |
+
st.divider()
|
91 |
+
|
92 |
+
LEFT, RIGHT = st.columns(2)
|
93 |
+
|
94 |
+
with LEFT:
|
95 |
+
num_videos = st.number_input(label='How many videos do you want to generate?',
|
96 |
+
min_value=1, max_value=10, value=1, step=1)
|
97 |
+
|
98 |
+
subreddit = st.text_input(
|
99 |
+
label='What Subreddit do you want to use', placeholder='AskReddit')
|
100 |
+
|
101 |
+
nsfw = st.checkbox(label='NSFW content?', value=False)
|
102 |
+
|
103 |
+
max_chars = st.slider(label='Maximum number of characters per line',
|
104 |
+
min_value=10, max_value=50, value=38, step=1)
|
105 |
+
|
106 |
+
max_words = st.number_input(label='Maximum number of words per line', min_value=1,
|
107 |
+
max_value=5, value=2, step=1)
|
108 |
+
|
109 |
+
result = st.button('Get subreddit')
|
110 |
+
|
111 |
+
with RIGHT:
|
112 |
+
if result:
|
113 |
+
create_instance(client_id=client_id, client_secret=client_secret,
|
114 |
+
user_agent=user_agent, subreddit=subreddit, nsfw=nsfw)
|
115 |
+
submission = st.session_state['submission']
|
116 |
+
title = submission.title
|
117 |
+
submission.comment_sort = "new"
|
118 |
+
top_level_comments = list(submission.comments)
|
119 |
+
max_comments = 10
|
120 |
+
st.subheader(title)
|
121 |
+
for comment in top_level_comments[:max_comments]:
|
122 |
+
st.text(comment.body)
|
123 |
+
st.divider()
|
src/__init__.py
ADDED
File without changes
|
src/arg_parser.py
ADDED
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import argparse
|
2 |
+
import sys
|
3 |
+
|
4 |
+
# voice_manager.py
|
5 |
+
from src.voice_manager import VoicesManager
|
6 |
+
|
7 |
+
import msg
|
8 |
+
from utils import rgb_to_bgr
|
9 |
+
|
10 |
+
|
11 |
+
async def parse_args():
|
12 |
+
parser = argparse.ArgumentParser()
|
13 |
+
parser.add_argument("--model", default="small", help="Model to use",
|
14 |
+
choices=["tiny", "base", "small", "medium", "large"], type=str)
|
15 |
+
parser.add_argument("--non_english", action='store_true',
|
16 |
+
help="Don't use the english model.")
|
17 |
+
parser.add_argument("--url", metavar='U', default="https://www.youtube.com/watch?v=intRX7BRA90",
|
18 |
+
help="Youtube URL to download as background video.", type=str)
|
19 |
+
parser.add_argument("--tts", default="en-US-ChristopherNeural",
|
20 |
+
help="Voice to use for TTS", type=str)
|
21 |
+
parser.add_argument(
|
22 |
+
"--list-voices", help="Use `edge-tts --list-voices` to list all voices", action='help')
|
23 |
+
parser.add_argument("--random_voice", action='store_true',
|
24 |
+
help="Random voice for TTS", default=False)
|
25 |
+
parser.add_argument("--gender", choices=["Male", "Female"],
|
26 |
+
help="Gender of the random TTS voice", type=str)
|
27 |
+
parser.add_argument(
|
28 |
+
"--language", help="Language of the random TTS voice for example: en-US", type=str)
|
29 |
+
parser.add_argument("--sub_format",
|
30 |
+
help="Subtitle format", choices=["u", "i", "b"], default="b", type=str)
|
31 |
+
parser.add_argument("--sub_position",
|
32 |
+
help="Subtitle position", choices=[i for i in range(1, 10)], default=5, type=int)
|
33 |
+
parser.add_argument("--font", help="Subtitle font",
|
34 |
+
default="Lexend Bold", type=str)
|
35 |
+
parser.add_argument("--font_color", help="Subtitle font color in hex format: FFF000",
|
36 |
+
default="FFF000", type=str)
|
37 |
+
parser.add_argument(
|
38 |
+
"--font_size", help="Subtitle font size", default=21, type=int)
|
39 |
+
parser.add_argument('--max_characters', default=38,
|
40 |
+
type=int, help='Max characters per line')
|
41 |
+
parser.add_argument('--max_words', default=2, type=int,
|
42 |
+
help='Max words per segment')
|
43 |
+
parser.add_argument("--upload_tiktok", help="Upload to TikTok after creating the video",
|
44 |
+
action='store_true', default=False)
|
45 |
+
parser.add_argument("-v", "--verbose", action='store_true',
|
46 |
+
help="Verbose")
|
47 |
+
args = parser.parse_args()
|
48 |
+
|
49 |
+
if args.random_voice: # Random voice
|
50 |
+
args.tts = None
|
51 |
+
if not args.gender:
|
52 |
+
print(
|
53 |
+
f"{msg.ERROR}When using --random_voice, please specify both --gender and --language arguments.")
|
54 |
+
sys.exit(1)
|
55 |
+
|
56 |
+
elif not args.language:
|
57 |
+
print(
|
58 |
+
f"{msg.ERROR}When using --random_voice, please specify both --gender and --language arguments.")
|
59 |
+
sys.exit(1)
|
60 |
+
|
61 |
+
elif args.gender and args.language:
|
62 |
+
# Check if voice is valid
|
63 |
+
voices_manager_obj = await VoicesManager().create()
|
64 |
+
voices = await VoicesManager().find(voices_manager_obj, args.gender, args.language)
|
65 |
+
args.tts = voices['Name']
|
66 |
+
|
67 |
+
# Check if language is english
|
68 |
+
if not str(args.language).startswith('en'):
|
69 |
+
args.non_english = True
|
70 |
+
|
71 |
+
else:
|
72 |
+
# Check if voice is valid
|
73 |
+
voices = await VoicesManager().create()
|
74 |
+
args.language = '-'.join(i for i in args.tts.split('-')[0:2])
|
75 |
+
voices = voices.find(Locale=args.language)
|
76 |
+
if len(voices) == 0:
|
77 |
+
# Voice not found
|
78 |
+
print(
|
79 |
+
f"{msg.ERROR}Specified TTS voice not found. Use `edge-tts --list-voices` to list all voices.")
|
80 |
+
sys.exit(1)
|
81 |
+
|
82 |
+
# Extract language from TTS voice
|
83 |
+
if args.tts:
|
84 |
+
lang_prefix = args.tts.split('-')[0]
|
85 |
+
if not lang_prefix.startswith('en'):
|
86 |
+
args.non_english = True
|
87 |
+
|
88 |
+
# Cast font color to lowercase
|
89 |
+
args.font_color = args.font_color.lower()
|
90 |
+
|
91 |
+
# Remove # from font color
|
92 |
+
if args.font_color.startswith('#'):
|
93 |
+
args.font_color = args.font_color[1:]
|
94 |
+
|
95 |
+
# Convert font color from RGB to BGR
|
96 |
+
args.font_color = rgb_to_bgr(args.font_color)
|
97 |
+
|
98 |
+
return args
|
src/logger.py
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import datetime
|
3 |
+
import logging
|
4 |
+
from pathlib import Path
|
5 |
+
|
6 |
+
|
7 |
+
class KeepDir:
|
8 |
+
def __init__(self):
|
9 |
+
self.original_dir = os.getcwd()
|
10 |
+
|
11 |
+
def __enter__(self):
|
12 |
+
return self
|
13 |
+
|
14 |
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
15 |
+
os.chdir(self.original_dir)
|
16 |
+
|
17 |
+
def chdir(self, path):
|
18 |
+
os.chdir(path)
|
19 |
+
|
20 |
+
|
21 |
+
def setup_logger():
|
22 |
+
HOME = Path.cwd()
|
23 |
+
log_directory = HOME / 'log'
|
24 |
+
if not log_directory.exists():
|
25 |
+
log_directory.mkdir()
|
26 |
+
|
27 |
+
with KeepDir() as keep_dir:
|
28 |
+
keep_dir.chdir(log_directory)
|
29 |
+
log_filename = f'{datetime.date.today()}.log'
|
30 |
+
logging.basicConfig(
|
31 |
+
level=logging.INFO,
|
32 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
33 |
+
handlers=[
|
34 |
+
logging.FileHandler(log_filename),
|
35 |
+
]
|
36 |
+
)
|
37 |
+
logger = logging.getLogger(__name__)
|
38 |
+
return logger
|
src/subtitle_creator.py
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from pathlib import Path
|
3 |
+
import torch
|
4 |
+
|
5 |
+
|
6 |
+
def srt_create(whisper_model, path: str, series: str, part: int, text: str, filename: str, **kwargs) -> bool:
|
7 |
+
series = series.replace(' ', '_')
|
8 |
+
|
9 |
+
srt_path = f"{path}{os.sep}{series}{os.sep}"
|
10 |
+
srt_filename = f"{srt_path}{series}_{part}.srt"
|
11 |
+
ass_filename = f"{srt_path}{series}_{part}.ass"
|
12 |
+
|
13 |
+
absolute_srt_path = Path(srt_filename).absolute()
|
14 |
+
absolute_ass_path = Path(ass_filename).absolute()
|
15 |
+
|
16 |
+
word_dict = {
|
17 |
+
'Fontname': kwargs.get('font', 'Arial'),
|
18 |
+
'Alignment': kwargs.get('sub_position', 5),
|
19 |
+
'BorderStyle': '1',
|
20 |
+
'Outline': '1',
|
21 |
+
'Shadow': '2',
|
22 |
+
'Blur': '21',
|
23 |
+
'Fontsize': kwargs.get('font_size', 21),
|
24 |
+
'MarginL': '0',
|
25 |
+
'MarginR': '0',
|
26 |
+
}
|
27 |
+
|
28 |
+
transcribe = whisper_model.transcribe(
|
29 |
+
filename, regroup=True, fp16=torch.cuda.is_available())
|
30 |
+
|
31 |
+
transcribe.split_by_gap(0.5).split_by_length(kwargs.get(
|
32 |
+
'max_characters')).merge_by_gap(0.15, max_words=kwargs.get('max_words'))
|
33 |
+
|
34 |
+
transcribe.to_srt_vtt(str(absolute_srt_path), word_level=True)
|
35 |
+
transcribe.to_ass(str(absolute_ass_path), word_level=True,
|
36 |
+
highlight_color=kwargs.get('font_color'), **word_dict)
|
37 |
+
return ass_filename
|
src/text_to_speech.py
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import edge_tts
|
2 |
+
|
3 |
+
|
4 |
+
async def tts(final_text: str, voice: str = "en-US-ChristopherNeural", stdout: bool = False, outfile: str = "tts.mp3", args=None) -> bool:
|
5 |
+
communicate = edge_tts.Communicate(final_text, voice)
|
6 |
+
if not stdout:
|
7 |
+
await communicate.save(outfile)
|
8 |
+
return True
|
src/tiktok.py
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import os
|
3 |
+
|
4 |
+
from tiktok_uploader.upload import upload_video
|
5 |
+
|
6 |
+
logger = logging.getLogger(__name__)
|
7 |
+
|
8 |
+
|
9 |
+
def upload_tiktok(file, title: str, tags: list, headless: bool = False):
|
10 |
+
if not os.path.isfile('cookies.txt'):
|
11 |
+
logger.error('Cookie file not found')
|
12 |
+
|
13 |
+
else:
|
14 |
+
logger.info('Cookie file found')
|
15 |
+
|
16 |
+
if len(tags) > 0:
|
17 |
+
tags = ' '.join([f"#{tag}" for tag in tags])
|
18 |
+
description = f"{title} {tags}"
|
19 |
+
else:
|
20 |
+
description = title
|
21 |
+
|
22 |
+
try:
|
23 |
+
upload_video(file, description=description, cookies='cookies.txt',
|
24 |
+
comment=True, stitch=False, duet=False, headless=headless)
|
25 |
+
|
26 |
+
except Exception as e:
|
27 |
+
logger.exception(e)
|
28 |
+
return False
|
29 |
+
|
30 |
+
return True
|
src/video_creator.py
ADDED
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
from pathlib import Path
|
3 |
+
|
4 |
+
import stable_whisper as whisper
|
5 |
+
from .logger import setup_logger
|
6 |
+
from .subtitle_creator import srt_create
|
7 |
+
from .text_to_speech import tts
|
8 |
+
from .tiktok import upload_tiktok
|
9 |
+
from .video_prepare import prepare_background
|
10 |
+
from .video_downloader import download_video as youtube_download
|
11 |
+
from utils import *
|
12 |
+
|
13 |
+
HOME = Path.cwd()
|
14 |
+
logger = setup_logger()
|
15 |
+
media_folder = HOME / 'media'
|
16 |
+
|
17 |
+
|
18 |
+
class VideoCreator:
|
19 |
+
def __init__(self, video, args):
|
20 |
+
self.args = args
|
21 |
+
self.video = video
|
22 |
+
|
23 |
+
self.series = video.get('series', '')
|
24 |
+
self.part = video.get('part', '')
|
25 |
+
self.text = video.get('text', '')
|
26 |
+
self.tags = video.get('tags', list())
|
27 |
+
self.outro = video.get('outro', '')
|
28 |
+
self.path = Path(media_folder).absolute()
|
29 |
+
|
30 |
+
def download_video(self, folder='background'):
|
31 |
+
youtube_download(url=self.args.url, folder=folder)
|
32 |
+
console.log(
|
33 |
+
f"{msg.OK}Video downloaded from {self.args.url} to {folder}")
|
34 |
+
logger.info(f"Video downloaded from {self.args.url} to {folder}")
|
35 |
+
|
36 |
+
def load_model(self):
|
37 |
+
model = self.args.model
|
38 |
+
if self.args.model != "large" and not self.args.non_english:
|
39 |
+
model = self.args.model + ".en"
|
40 |
+
whisper_model = whisper.load_model(model)
|
41 |
+
|
42 |
+
self.model = whisper_model
|
43 |
+
return whisper_model
|
44 |
+
|
45 |
+
def create_text(self):
|
46 |
+
req_text = f"{self.series} - Part {self.part}.\n{self.text}\n{self.outro}"
|
47 |
+
series = self.series.replace(' ', '_')
|
48 |
+
filename = f"{self.path}{os.sep}{series}{os.sep}{series}_{self.part}.mp3"
|
49 |
+
|
50 |
+
Path(f"{self.path}{os.sep}{series}").mkdir(parents=True, exist_ok=True)
|
51 |
+
|
52 |
+
self.req_text = req_text
|
53 |
+
self.mp3_file = filename
|
54 |
+
return req_text, filename
|
55 |
+
|
56 |
+
async def text_to_speech(self):
|
57 |
+
await tts(self.req_text, outfile=self.mp3_file, voice=self.args.tts, args=self.args)
|
58 |
+
|
59 |
+
def generate_transcription(self):
|
60 |
+
ass_filename = srt_create(self.model,
|
61 |
+
self.path, self.series, self.part, self.text, self.mp3_file, **vars(self.args))
|
62 |
+
ass_filename = Path(ass_filename).absolute()
|
63 |
+
|
64 |
+
self.ass_file = ass_filename
|
65 |
+
return ass_filename
|
66 |
+
|
67 |
+
def select_background(self):
|
68 |
+
try:
|
69 |
+
# Background video selected with WebUI
|
70 |
+
background_mp4 = self.args.mp4_background
|
71 |
+
|
72 |
+
with KeepDir() as keep_dir:
|
73 |
+
keep_dir.chdir("background")
|
74 |
+
background_mp4 = Path(background_mp4).absolute()
|
75 |
+
except AttributeError:
|
76 |
+
# CLI execution
|
77 |
+
background_mp4 = random_background()
|
78 |
+
|
79 |
+
background_mp4 = str(Path(background_mp4).absolute())
|
80 |
+
|
81 |
+
self.mp4_background = background_mp4
|
82 |
+
return background_mp4
|
83 |
+
|
84 |
+
def integrate_subtitles(self):
|
85 |
+
final_video = prepare_background(
|
86 |
+
self.mp4_background, filename_mp3=self.mp3_file, filename_srt=self.ass_file, verbose=self.args.verbose)
|
87 |
+
final_video = Path(final_video).absolute()
|
88 |
+
|
89 |
+
self.mp4_final_video = final_video
|
90 |
+
return final_video
|
91 |
+
|
92 |
+
def upload_to_tiktok(self):
|
93 |
+
uploaded = upload_tiktok(str(
|
94 |
+
self.mp4_final_video), title=f"{self.series} - {self.part}", tags=self.tags, headless=not self.args.verbose)
|
95 |
+
return uploaded
|
src/video_downloader.py
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import subprocess
|
2 |
+
from pathlib import Path
|
3 |
+
|
4 |
+
import msg
|
5 |
+
from utils import KeepDir
|
6 |
+
|
7 |
+
HOME = Path.cwd()
|
8 |
+
|
9 |
+
|
10 |
+
def download_video(url: str, folder: str = 'background'):
|
11 |
+
"""
|
12 |
+
Downloads a video from the given URL and saves it to the specified folder.
|
13 |
+
|
14 |
+
Args:
|
15 |
+
url (str): The URL of the video to download.
|
16 |
+
folder (str, optional): The name of the folder to save the video in. Defaults to 'background'.
|
17 |
+
"""
|
18 |
+
directory = HOME / folder
|
19 |
+
if not directory.exists():
|
20 |
+
directory.mkdir()
|
21 |
+
|
22 |
+
with KeepDir() as keep_dir:
|
23 |
+
keep_dir.chdir(folder)
|
24 |
+
subprocess.run(['yt-dlp', '-f bestvideo[ext=mp4]',
|
25 |
+
'--restrict-filenames', url], check=True)
|
src/video_prepare.py
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import multiprocessing
|
2 |
+
import os
|
3 |
+
import subprocess
|
4 |
+
import random
|
5 |
+
|
6 |
+
from utils import *
|
7 |
+
|
8 |
+
HOME = Path.cwd()
|
9 |
+
|
10 |
+
|
11 |
+
def prepare_background(background_mp4: str, filename_mp3: str, filename_srt: str, verbose: bool = False) -> str:
|
12 |
+
video_info = get_info(background_mp4, kind='video')
|
13 |
+
video_duration = int(round(video_info.get('duration'), 0))
|
14 |
+
|
15 |
+
audio_info = get_info(filename_mp3, kind='audio')
|
16 |
+
audio_duration = int(round(audio_info.get('duration'), 0))
|
17 |
+
|
18 |
+
ss = random.randint(0, (video_duration-audio_duration))
|
19 |
+
audio_duration = convert_time(audio_duration)
|
20 |
+
if ss < 0:
|
21 |
+
ss = 0
|
22 |
+
|
23 |
+
srt_raw = filename_srt
|
24 |
+
srt_filename = filename_srt.name
|
25 |
+
srt_path = filename_srt.parent.absolute()
|
26 |
+
|
27 |
+
directory = HOME / 'output'
|
28 |
+
if not directory.exists():
|
29 |
+
directory.mkdir()
|
30 |
+
|
31 |
+
outfile = f"{HOME}{os.sep}output{os.sep}output_{ss}.mp4"
|
32 |
+
|
33 |
+
if verbose:
|
34 |
+
rich_print(
|
35 |
+
f"{filename_srt = }\n{background_mp4 = }\n{filename_mp3 = }\n", style='bold green')
|
36 |
+
|
37 |
+
args = [
|
38 |
+
"ffmpeg",
|
39 |
+
"-ss", str(ss),
|
40 |
+
"-t", str(audio_duration),
|
41 |
+
"-i", background_mp4,
|
42 |
+
"-i", filename_mp3,
|
43 |
+
"-map", "0:v",
|
44 |
+
"-map", "1:a",
|
45 |
+
"-vf", f"crop=ih/16*9:ih, scale=w=1080:h=1920:flags=lanczos, gblur=sigma=2, ass='{srt_raw.absolute()}'",
|
46 |
+
"-c:v", "libx264",
|
47 |
+
"-crf", "23",
|
48 |
+
"-c:a", "aac",
|
49 |
+
"-ac", "2",
|
50 |
+
"-b:a", "192K",
|
51 |
+
f"{outfile}",
|
52 |
+
"-y",
|
53 |
+
"-threads", f"{multiprocessing.cpu_count()}"]
|
54 |
+
|
55 |
+
if verbose:
|
56 |
+
rich_print('[i] FFMPEG Command:\n'+' '.join(args)+'\n', style='yellow')
|
57 |
+
|
58 |
+
with KeepDir() as keep_dir:
|
59 |
+
keep_dir.chdir(srt_path)
|
60 |
+
subprocess.run(args, check=True)
|
61 |
+
|
62 |
+
return outfile
|
src/voice_manager.py
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sys
|
2 |
+
|
3 |
+
import edge_tts
|
4 |
+
|
5 |
+
|
6 |
+
class VoicesManager:
|
7 |
+
@staticmethod
|
8 |
+
async def create():
|
9 |
+
return await edge_tts.VoicesManager.create()
|
10 |
+
|
11 |
+
@staticmethod
|
12 |
+
def find(voices, Gender, Locale):
|
13 |
+
voices = voices.find(Gender=Gender, Locale=Locale)
|
14 |
+
if len(voices) == 0:
|
15 |
+
print(f"Specified TTS language not found. Make sure you are using the correct format. For example: en-US")
|
16 |
+
sys.exit(1)
|
17 |
+
return voices['Name']
|