Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add stable-ts to requirements.txt and update video.json structure #16

Merged

Conversation

MatteoFasulo
Copy link
Owner

@MatteoFasulo MatteoFasulo commented Dec 29, 2023

Summary by CodeRabbit

  • New Features

    • Integrated a new text-to-speech engine.
    • Added subtitle creation and formatting capabilities.
    • Implemented video downloading functionality.
    • Introduced video preparation with background audio and subtitles.
    • Added a voice manager to select voices based on user preferences.
    • Launched video creation and processing features, including TikTok uploading.
  • Enhancements

    • Improved the modularity of the codebase with new arg_parser and video_creator modules.
    • Transitioned to asynchronous file operations for enhanced performance.
    • Streamlined command-line argument parsing with new options and validations.
  • Dependencies

    • Added stable-ts and tiktok-uploader packages for advanced functionality.
  • Refactor

    • Reorganized code for better readability and maintainability.
    • Upgraded logging setup for more efficient troubleshooting and monitoring.

@MatteoFasulo MatteoFasulo linked an issue Dec 29, 2023 that may be closed by this pull request
Copy link

coderabbitai bot commented Dec 29, 2023

Walkthrough

Walkthrough

The project underwent a significant refactoring for better modularity and maintainability. Key improvements include the introduction of asynchronous file operations, modular argument parsing, enhanced video creation and processing, and direct TikTok uploading capabilities. The addition of new dependencies supports these features, streamlining the video production pipeline from content generation to publication.

Changes

File(s) Summary of Changes
main.py Refactored for modularity with new arg_parser and video_creator modules; added async file ops.
requirements.txt Added stable-ts and tiktok-uploader packages.
src/arg_parser.py, src/logger.py Introduced argument parsing and logging setup functions.
src/subtitle_creator.py, src/.../tts.py, src/voice_manager.py Functions for subtitles, TTS, and voice management.
src/tiktok.py, src/video_downloader.py, src/video_prepare.py TikTok uploading, video downloading, and background preparation functions.
src/video_creator.py Added VideoCreator class for video processing and TikTok uploading.
utils.py Enhanced with new utility functions and imports.

Poem

🐰✨
In the burrows of code, where the digital hops,
A refactor blooms, and redundancy drops.
From text to speech, to TikTok's bright stage,
A rabbit's craft turns a new page.
🎉📝

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

Share

Tips

Chat with CodeRabbit Bot (@coderabbitai)

  • You can directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>.
    • Generate unit-tests for this file.
  • You can tag CodeRabbit on specific lines of code or entire files in the PR by tagging @coderabbitai in a comment. Examples:
    • @coderabbitai generate unit tests for this file.
    • @coderabbitai modularize this function.
  • You can tag @coderabbitai in a PR comment and ask questions about the PR and the codebase. Examples:
    • @coderabbitai generate interesting stats about this repository from git and render them as a table.
    • @coderabbitai show all the console.log statements in this repository.
    • @coderabbitai read src/utils.ts and generate unit tests.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid.
    • @coderabbitai read the files in the src/scheduler package and generate README in the markdown format.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks.

CodeRabbit Commands (invoked as PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger a review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai help to get help.

Additionally, you can add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.

CodeRabbit Configration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • The JSON schema for the configuration file is available here.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/coderabbit-overrides.v2.json

CodeRabbit Discord Community

Join our Discord Community to get help, request features, and share feedback.

@MatteoFasulo MatteoFasulo merged commit 5f24e81 into main Dec 29, 2023
@MatteoFasulo MatteoFasulo deleted the 15-webui-gradio-issue-all-the-project-is-on-one-file-mainpy branch December 29, 2023 15:08
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 13

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 724cfdb and b344d57.
Files ignored due to filter (1)
  • video.json
Files selected for processing (12)
  • main.py (4 hunks)
  • requirements.txt (1 hunks)
  • src/arg_parser.py (1 hunks)
  • src/logger.py (1 hunks)
  • src/subtitle_creator.py (1 hunks)
  • src/text_to_speech.py (1 hunks)
  • src/tiktok.py (1 hunks)
  • src/video_creator.py (1 hunks)
  • src/video_downloader.py (1 hunks)
  • src/video_prepare.py (1 hunks)
  • src/voice_manager.py (1 hunks)
  • utils.py (1 hunks)
Files skipped from review due to trivial changes (1)
  • requirements.txt
Additional comments: 1
utils.py (1)
  • 101-115: The convert_time function converts time in seconds to a formatted string. The function is correct, but it's not clear if it's used in the context of the application. Ensure that this utility function is necessary and used within the codebase.

Comment on lines +4 to +8
async def tts(final_text: str, voice: str = "en-US-ChristopherNeural", stdout: bool = False, outfile: str = "tts.mp3", args=None) -> bool:
communicate = edge_tts.Communicate(final_text, voice)
if not stdout:
await communicate.save(outfile)
return True
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tts function correctly uses the edge_tts package to perform text-to-speech operations. The function signature is clear, and the use of type hints improves readability. The function always returns True, which might not be informative if the operation fails. Consider returning a more descriptive result or raising an exception on failure.

Comment on lines +6 to +17
class VoicesManager:
@staticmethod
async def create():
return await edge_tts.VoicesManager.create()

@staticmethod
def find(voices, Gender, Locale):
voices = voices.find(Gender=Gender, Locale=Locale)
if len(voices) == 0:
print(f"Specified TTS language not found. Make sure you are using the correct format. For example: en-US")
sys.exit(1)
return voices['Name']
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The find method in the VoicesManager class exits the program if no voices are found. This could be problematic for a library function, as it takes control away from the calling code. Consider raising an exception instead of exiting directly to allow the caller to handle the situation appropriately.

Comment on lines +11 to +27
def download_video(url: str, folder: str = 'background'):
"""
Downloads a video from the given URL and saves it to the specified folder.

Args:
url (str): The URL of the video to download.
folder (str, optional): The name of the folder to save the video in. Defaults to 'background'.
"""
directory = HOME / folder
if not directory.exists():
directory.mkdir()

with KeepDir() as keep_dir:
keep_dir.chdir(folder)
subprocess.run(['yt-dlp', '-f bestvideo[ext=mp4]+bestaudio[ext=m4a]',
'--restrict-filenames', url], check=True)
print(f"{msg.OK}Background video downloaded successfully")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The download_video function uses subprocess.run to invoke yt-dlp, which is a blocking call. Since the rest of the codebase is moving towards asynchronous operations, consider using an asynchronous library or running this in a separate thread or process to avoid blocking the main event loop.

Comment on lines +9 to +30
def upload_tiktok(file, title: str, tags: list, headless: bool = False):
if not os.path.isfile('cookies.txt'):
logger.error('Cookie file not found')

else:
logger.info('Cookie file found')

if len(tags) > 0:
tags = ' '.join([f"#{tag}" for tag in tags])
description = f"{title} {tags}"
else:
description = title

try:
upload_video(file, description=description, cookies='cookies.txt',
comment=True, stitch=False, duet=False, headless=headless)

except Exception as e:
logger.exception(e)
return False

return True
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The upload_tiktok function checks for the existence of a 'cookies.txt' file but does not handle the case where the file is not found, other than logging an error. It would be more robust to raise an exception or return an error value to allow the calling code to handle this situation.

Comment on lines +21 to +38
def setup_logger():
HOME = Path.cwd()
log_directory = HOME / 'log'
if not log_directory.exists():
log_directory.mkdir()

with KeepDir() as keep_dir:
keep_dir.chdir(log_directory)
log_filename = f'{datetime.date.today()}.log'
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(log_filename),
]
)
logger = logging.getLogger(__name__)
return logger
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setup_logger function sets up logging correctly. However, it changes the current working directory, which could have unintended side effects on other parts of the application that rely on the current directory. Consider using absolute paths for file handling instead of changing the working directory.

Comment on lines +11 to +57
def prepare_background(background_mp4: str, filename_mp3: str, filename_srt: str, verbose: bool = False) -> str:
video_duration = int(round(get_info(background_mp4).get('duration'), 0))
audio_info = get_info(filename_mp3)
audio_duration = int(round(audio_info.get('duration'), 0))
ss = random.randint(0, (video_duration-audio_duration))
audio_duration = convert_time(audio_info.get('duration'))
if ss < 0:
ss = 0

srt_filename = filename_srt.name
srt_path = filename_srt.parent.absolute()

directory = HOME / 'output'
if not directory.exists():
directory.mkdir()

outfile = f"{HOME}{os.sep}output{os.sep}output_{ss}.mp4"

if verbose:
rich_print(
f"{filename_srt = }\n{background_mp4 = }\n{filename_mp3 = }\n", style='bold green')

args = [
"ffmpeg",
"-ss", str(ss),
"-t", str(audio_duration),
"-i", background_mp4,
"-i", filename_mp3,
"-map", "0:v",
"-map", "1:a",
"-filter:v",
f"crop=ih/16*9:ih, scale=w=1080:h=1920:flags=bicubic, gblur=sigma=2, subtitles={srt_filename}:force_style=',Alignment=8,BorderStyle=7,Outline=3,Shadow=5,Blur=15,Fontsize=15,MarginL=45,MarginR=55,FontName=Lexend Bold'",
"-c:v", "libx264", "-preset", "5",
"-b:v", "5M",
"-c:a", "aac", "-ac", "1",
"-b:a", "96K",
f"{outfile}", "-y",
"-threads", f"{multiprocessing.cpu_count()//2}"]

if verbose:
rich_print('[i] FFMPEG Command:\n'+' '.join(args)+'\n', style='yellow')

with KeepDir() as keep_dir:
keep_dir.chdir(srt_path)
subprocess.run(args, check=True)

return outfile
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prepare_background function uses ffmpeg to process videos. It uses a blocking subprocess.run call within an asynchronous codebase. Consider using an asynchronous approach to running ffmpeg to avoid blocking the event loop.

Comment on lines +10 to +77
async def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--model", default="small", help="Model to use",
choices=["tiny", "base", "small", "medium", "large"], type=str)
parser.add_argument("--non_english", action='store_true',
help="Don't use the english model.")
parser.add_argument("--url", metavar='U', default="https://www.youtube.com/watch?v=intRX7BRA90",
help="Youtube URL to download as background video.", type=str)
parser.add_argument("--tts", default="en-US-ChristopherNeural",
help="Voice to use for TTS", type=str)
parser.add_argument(
"--list-voices", help="Use `edge-tts --list-voices` to list all voices", action='help')
parser.add_argument("--random_voice", action='store_true',
help="Random voice for TTS", default=False)
parser.add_argument("--gender", choices=["Male", "Female"],
help="Gender of the random TTS voice", type=str)
parser.add_argument(
"--language", help="Language of the random TTS voice for example: en-US", type=str)
parser.add_argument("--sub_format",
help="Subtitle format", choices=["u", "i", "b"], default="b", type=str)
parser.add_argument("--font_color", help="Subtitle font color in hex format: #FFF000",
default="#FFF000", type=str)
parser.add_argument("--upload_tiktok", help="Upload to TikTok after creating the video",
action='store_true', default=False)
parser.add_argument("-v", "--verbose", action='store_true',
help="Verbose")
args = parser.parse_args()

if args.random_voice: # Random voice
args.tts = None
if not args.gender:
print(
f"{msg.ERROR}When using --random_voice, please specify both --gender and --language arguments.")
sys.exit(1)

elif not args.language:
print(
f"{msg.ERROR}When using --random_voice, please specify both --gender and --language arguments.")
sys.exit(1)

elif args.gender and args.language:
# Check if voice is valid
voices_manager_obj = await VoicesManager().create()
voices = await VoicesManager().find(voices_manager_obj, args.gender, args.language)
args.tts = voices['Name']

# Check if language is english
if not str(args.language).startswith('en'):
args.non_english = True

else:
# Check if voice is valid
voices = await VoicesManager().create()
args.language = '-'.join(i for i in args.tts.split('-')[0:2])
voices = voices.find(Locale=args.language)
if len(voices) == 0:
# Voice not found
print(
f"{msg.ERROR}Specified TTS voice not found. Use `edge-tts --list-voices` to list all voices.")
sys.exit(1)

# Extract language from TTS voice
if args.tts:
lang_prefix = args.tts.split('-')[0]
if not lang_prefix.startswith('en'):
args.non_english = True

return args
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parse_args function is marked as asynchronous but does not perform any I/O operations that would benefit from being asynchronous. Consider removing the async keyword to simplify the code unless there is a specific reason for this function to be asynchronous.

Comment on lines +20 to +95
class VideoCreator:
def __init__(self, args):
self.args = args

self.series = jsonData.get('series', '')
self.part = jsonData.get('part', '')
self.text = jsonData.get('text', '')
self.tags = jsonData.get('tags', list())
self.outro = jsonData.get('outro', '')
self.path = Path(media_folder).absolute()

def download_video(self, folder: str = 'background'):
directory = HOME / folder
if not directory.exists():
directory.mkdir()

with KeepDir() as keep_dir:
keep_dir.chdir(folder)
subprocess.run(['yt-dlp', '-f bestvideo[ext=mp4]+bestaudio[ext=m4a]',
'--restrict-filenames', self.args.url], check=True)
console.log(
f"{msg.OK}Video downloaded from {self.args.url} to {folder}")
logger.info(f"Video downloaded from {self.args.url} to {folder}")

def load_model(self):
model = self.args.model
if self.args.model != "large" and not self.args.non_english:
model = self.args.model + ".en"
whisper_model = whisper.load_model(model)

self.model = whisper_model
return whisper_model

def create_text(self):
req_text = f"{self.series} - {self.part}.\n{self.text}\n{self.outro}"
series = self.series.replace(' ', '_')
filename = f"{self.path}{os.sep}{series}{os.sep}{series}_{self.part}.mp3"

Path(f"{self.path}{os.sep}{series}").mkdir(parents=True, exist_ok=True)

self.req_text = req_text
self.mp3_file = filename
return req_text, filename

async def text_to_speech(self):
await tts(self.req_text, outfile=self.mp3_file, voice=self.args.tts, args=self.args)

def generate_transcription(self):
srt_filename = srt_create(
self.model, self.path, self.series, self.part, self.text, self.mp3_file)
srt_filename = Path(srt_filename).absolute()

self.srt_file = srt_filename

highlight_words(self.srt_file, subtitle_format=self.args.sub_format,
font_color=self.args.font_color)
return srt_filename

def select_background(self):
background_mp4 = random_background()

self.mp4_backgroung = background_mp4
return background_mp4

def integrate_subtitles(self):
final_video = prepare_background(
self.mp4_backgroung, filename_mp3=self.mp3_file, filename_srt=self.srt_file, verbose=self.args.verbose)
final_video = Path(final_video).absolute()

self.mp4_final_video = final_video
return final_video

def upload_to_tiktok(self):
uploaded = upload_tiktok(str(
self.mp4_final_video), title=f"{self.series} - {self.part}", tags=self.tags, headless=not self.args.verbose)
return uploaded
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The VideoCreator class methods are well-structured and modular. However, the class relies on global variables and reads from a JSON file upon instantiation. Consider passing the necessary data as parameters to the class constructor to improve testability and maintainability.

Comment on lines +36 to +55
"""
Returns the filename of a random file in the specified folder.

Args:
folder(str): The folder containing the files.

Returns:
str: The filename of a randomly selected file in the folder.
"""
directory = Path(folder).absolute()
if not directory.exists():
directory.mkdir()

with KeepDir() as keep_dir:
keep_dir.chdir(folder)
files = os.listdir(".")
random_file = random.choice(files)
return Path(random_file).absolute()

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The random_background function selects a random file from a specified directory. It assumes that all files in the directory are valid backgrounds. Consider adding a check to ensure that only appropriate file types (e.g., video files) are selected.

Comment on lines +57 to +98
"""
Get information about a video file.

Args:
filename (str): The path to the video file.
verbose (bool, optional): Whether to print verbose output. Defaults to False.

Returns:
dict: A dictionary containing information about the video file, including width, height, bit rate, and duration.
"""
try:
probe = ffmpeg.probe(filename)
video_stream = next(
(stream for stream in probe['streams'] if stream['codec_type'] == 'video'), None)
audio_stream = next(
(stream for stream in probe['streams'] if stream['codec_type'] == 'audio'), None)
try:
duration = float(audio_stream['duration'])
except Exception:
if verbose:
console.log(
f"{msg.WARNING}MP4 default metadata not found")
logger.warning('MP4 default metadata not found')
duration = (datetime.datetime.strptime(
audio_stream['DURATION'], '%H:%M:%S.%f') - datetime.datetime.min).total_seconds()
if video_stream is None:
if verbose:
console.log(
f"{msg.WARNING}No video stream found")
logger.warning('No video stream found')
bit_rate = int(audio_stream['bit_rate'])
return {'bit_rate': bit_rate, 'duration': duration}

width = int(video_stream['width'])
height = int(video_stream['height'])
return {'width': width, 'height': height, 'duration': duration}

except ffmpeg.Error as e:
console.log(f"{msg.ERROR}{e.stderr}")
logger.exception(e.stderr)
sys.exit(1)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The get_info function retrieves video file information using ffmpeg. It exits the program if an error occurs, which is not ideal for a utility function. Instead, consider raising an exception to allow the caller to handle the error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[WebUI] Gradio issue - all the project is on one file (main.py)
1 participant