MatteoFasulo commited on
Commit
cf72e88
·
1 Parent(s): bfb1bc2

Added src folder

Browse files
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']