Skip to content

Commit

Permalink
Fixed os compatibility for temp folder, improved lyrics cleaning, mad…
Browse files Browse the repository at this point in the history
…e finalise more robust with cleaner logging, made default log output quieter
  • Loading branch information
beveradb committed Dec 21, 2023
1 parent 27946a1 commit 8924875
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 52 deletions.
47 changes: 25 additions & 22 deletions karaoke_finalise/karaoke_finalise.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,28 @@ def __init__(

self.logger.debug(f"KaraokeFinalise instantiating")

self.ffmpeg_base_command = "ffmpeg -hide_banner -nostats"

if self.log_level == logging.DEBUG:
self.ffmpeg_base_command += " -loglevel verbose"
else:
self.ffmpeg_base_command += " -loglevel fatal"

self.model_name = model_name

def process(self):
tracks = []

self.logger.info(f"Searching for files in current directory ending with (Karaoke).mov")
for karaoke_file in filter(lambda f: " (Karaoke).mov" in f, os.listdir(".")):
base_name = karaoke_file.replace(" (Karaoke).mov", "")
artist = base_name.split(" - ")[0]
title = base_name.split(" - ")[1]

with_vocals_file = f"{base_name} (With Vocals).mov"
title_file = f"{base_name} (Title).mov"
instrumental_file = f"{base_name} (Instrumental {self.model_name}).MP3"
instrumental_file = f"{base_name} (Instrumental {self.model_name}).mp3"

final_mp4_file = f"{base_name} (Final Karaoke).mp4"

track = {
Expand All @@ -50,36 +59,30 @@ def process(self):
}

if os.path.isfile(title_file) and os.path.isfile(karaoke_file) and os.path.isfile(instrumental_file):
print("Renaming karaoke file to WithVocals")
self.logger.info(f"All 3 input files found for {base_name}, beginning finalisation")

self.logger.info(f"Output [With Vocals]: renaming synced video to: {with_vocals_file}")
os.rename(karaoke_file, with_vocals_file)

print(f"Remuxing karaoke video with instrumental audio to '{karaoke_file}'")
subprocess.run(
["ffmpeg", "-an", "-i", with_vocals_file, "-vn", "-i", instrumental_file, "-c:v", "copy", "-c:a", "aac", karaoke_file]
)
self.logger.info(f"Output [With Instrumental]: remuxing synced video with instrumental audio to: {karaoke_file}")

ffmpeg_command = f'{self.ffmpeg_base_command} -an -i "{with_vocals_file}" -vn -i "{instrumental_file}" -c:v copy -c:a aac "{karaoke_file}"'
self.logger.debug(f"Running command: {ffmpeg_command}")
os.system(ffmpeg_command)

self.logger.info(f"Output [Final Karaoke]: joining title video and instrumental video to produce: {final_mp4_file}")

print(f"Joining '{title_file}' and '{karaoke_file}' into '{final_mp4_file}'")
with tempfile.NamedTemporaryFile(mode="w+", delete=False, dir="/tmp", suffix=".txt") as tmp_file_list:
tmp_file_list.write(f"file '{os.path.abspath(title_file)}'\n")
tmp_file_list.write(f"file '{os.path.abspath(karaoke_file)}'\n")
subprocess.run(
[
"ffmpeg",
"-f",
"concat",
"-safe",
"0",
"-i",
tmp_file_list.name,
"-vf",
"settb=AVTB,setpts=N/30/TB,fps=30",
final_mp4_file,
]
)

ffmpeg_command = f'{self.ffmpeg_base_command} -f concat -safe 0 -i "{tmp_file_list.name}" -vf settb=AVTB,setpts=N/30/TB,fps=30 "{final_mp4_file}"'
self.logger.debug(f"Running command: {ffmpeg_command}")
os.system(ffmpeg_command)

os.remove(tmp_file_list.name)
else:
print(f"Required files for '{base_name}' not found.")
self.logger.error(f"Unable to find all 3 required input files:\n {title_file}\n {karaoke_file}\n {instrumental_file}")

tracks.append(track)

Expand Down
86 changes: 62 additions & 24 deletions karaoke_prep/karaoke_prep.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import yt_dlp
import logging
import lyricsgenius
import subprocess
import tempfile
from PIL import Image, ImageDraw, ImageFont


Expand All @@ -18,7 +18,7 @@ def __init__(
log_formatter=None,
model_name="UVR_MDXNET_KARA_2",
model_name_2="UVR-MDX-NET-Inst_HQ_3",
model_file_dir="/tmp/audio-separator-models/",
model_file_dir=os.path.join(tempfile.gettempdir(), "audio-separator-models"),
output_dir=".",
output_format="WAV",
use_cuda=False,
Expand Down Expand Up @@ -145,6 +145,7 @@ def get_youtube_id_for_top_search_result(self, query):
def download_video(self, youtube_id, output_filename_no_extension):
self.logger.debug(f"Downloading YouTube video {youtube_id} to filename {output_filename_no_extension} + (as yet) unknown extension")
ydl_opts = {
"quiet": "True",
"format": "bv*+ba/b", # if a combined video + audio format is better than the best video-only format use the combined format
"outtmpl": f"{output_filename_no_extension}",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36",
Expand Down Expand Up @@ -173,7 +174,7 @@ def convert_to_wav(self, input_filename, output_filename_no_extension):
return output_filename

def write_lyrics_from_genius(self, artist, title, filename):
genius = lyricsgenius.Genius(os.environ["GENIUS_API_TOKEN"])
genius = lyricsgenius.Genius(access_token=os.environ["GENIUS_API_TOKEN"], verbose=False, remove_section_headers=True)
song = genius.search_song(title, artist)
if song:
lyrics = self.clean_genius_lyrics(song.lyrics)
Expand All @@ -192,6 +193,10 @@ def clean_genius_lyrics(self, lyrics):
lyrics = re.sub(
r".*?Lyrics([A-Z])", r"\1", lyrics
) # Remove the song name and word "Lyrics" if this has a non-newline char at the start
lyrics = re.sub(r"^[0-9]* Contributors.*Lyrics", "", lyrics) # Remove this example: 27 ContributorsSex Bomb Lyrics
lyrics = re.sub(
r"See.*Live.*Get tickets as low as \$[0-9]+", "", lyrics
) # Remove this example: See Tom Jones LiveGet tickets as low as $71
lyrics = re.sub(r"[0-9]+Embed$", "", lyrics) # Remove the word "Embed" at end of line with preceding numbers if found
lyrics = re.sub(r"(\S)Embed$", r"\1", lyrics) # Remove the word "Embed" if it has been tacked onto a word at the end of a line
lyrics = re.sub(r"^Embed$", r"", lyrics) # Remove the word "Embed" if it has been tacked onto a word at the end of a line
Expand Down Expand Up @@ -232,20 +237,53 @@ def find_best_split_point(self, line):
self.logger.debug(f"No comma or suitable 'and' found, using middle word as split point")
return len(" ".join(words[:mid_word_index]))

def process_line(self, line):
"""
Process a single line to ensure it's within the maximum length,
and handle parentheses.
"""
processed_lines = []
while len(line) > 36:
# Check if the line contains parentheses
if "(" in line and ")" in line:
start_paren = line.find("(")
end_paren = line.find(")") + 1
if end_paren < len(line) and line[end_paren] == ",":
end_paren += 1

if start_paren > 0:
processed_lines.append(line[:start_paren].strip())
processed_lines.append(line[start_paren:end_paren].strip())
line = line[end_paren:].strip()
else:
split_point = self.find_best_split_point(line)
processed_lines.append(line[:split_point].strip())
line = line[split_point:].strip()

if line: # Add the remaining part if not empty
processed_lines.append(line)

return processed_lines

def write_processed_lyrics(self, lyrics, processed_lyrics_file):
self.logger.debug(f"Writing processed lyrics to {processed_lyrics_file}")

with open(processed_lyrics_file, "w") as outfile:
all_processed = False
while not all_processed:
all_processed = True
new_lyrics = []
for line in lyrics:
line = line.strip()
processed = self.process_line(line)
new_lyrics.extend(processed)
if any(len(l) > 36 for l in processed):
all_processed = False
lyrics = new_lyrics

# Write the processed lyrics to file
for line in lyrics:
line = line.strip()
if len(line) > 40:
self.logger.debug(f"Line is longer than 40 characters, splitting at best split point: {line}")
split_point = self.find_best_split_point(line)
outfile.write(line[:split_point].strip() + "\n")
outfile.write(line[split_point:].strip() + "\n")
else:
self.logger.debug(f"Line is shorter than 40 characters, writing as-is: {line}")
outfile.write(line + "\n")
outfile.write(line + "\n")

def sanitize_filename(self, filename):
"""Replace or remove characters that are unsafe for filenames."""
Expand Down Expand Up @@ -406,6 +444,18 @@ def prep_single_track(self):
processed_track["title_video"] = os.path.join(track_output_dir, f"{artist_title} (Title).mov")
self.create_title_video(artist, title, self.title_format, processed_track["title_image"], processed_track["title_video"])

lyrics_file = os.path.join(track_output_dir, f"{artist_title} (Lyrics).txt")
processed_lyrics_file = os.path.join(track_output_dir, f"{artist_title} (Lyrics Processed).txt")
if os.path.exists(lyrics_file):
self.logger.debug(f"Lyrics file already exists, skipping fetch: {lyrics_file}")
else:
self.logger.info("Fetching lyrics from Genius...")
lyrics = self.write_lyrics_from_genius(artist, title, lyrics_file)
self.write_processed_lyrics(lyrics, processed_lyrics_file)

processed_track["lyrics"] = lyrics_file
processed_track["processed_lyrics"] = processed_lyrics_file

yt_webm_filename_pattern = os.path.join(track_output_dir, f"{artist_title} (YouTube *.webm")
yt_webm_glob = glob.glob(yt_webm_filename_pattern)

Expand Down Expand Up @@ -450,18 +500,6 @@ def prep_single_track(self):
else:
self.logger.warning(f"Skipping {title} by {artist} due to missing YouTube ID.")

lyrics_file = os.path.join(track_output_dir, f"{artist_title} (Lyrics).txt")
processed_lyrics_file = os.path.join(track_output_dir, f"{artist_title} (Lyrics Processed).txt")
if os.path.exists(lyrics_file):
self.logger.debug(f"Lyrics file already exists, skipping fetch: {lyrics_file}")
else:
self.logger.info("Fetching lyrics from Genius...")
lyrics = self.write_lyrics_from_genius(artist, title, lyrics_file)
self.write_processed_lyrics(lyrics, processed_lyrics_file)

processed_track["lyrics"] = lyrics_file
processed_track["processed_lyrics"] = processed_lyrics_file

self.logger.info(f"Separating audio twice for track: {title} by {artist}")

instrumental_path = os.path.join(track_output_dir, f"{artist_title} (Instrumental {self.model_name}).{self.output_format}")
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "karaoke-prep"
version = "0.6.4"
version = "0.7.0"
description = "Prepare for karaoke video creation, by downloading audio and lyrics for a specified song or youtube playlist and separatung audio stems, then finalise the video with a title screen after manual syncing!"
authors = ["Andrew Beveridge <andrew@beveridge.uk>"]
license = "MIT"
Expand Down
7 changes: 5 additions & 2 deletions utils/finalise_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,17 @@ def main():

tracks = kfinalise.process()

logger.info(f"Karaoke Finalisation complete! Output files:")
if len(tracks) == 0:
logger.error(f"No tracks found to process.")
return

logger.info(f"Karaoke finalisation processing complete! Output files:")
for track in tracks:
logger.info(f"")
logger.info(f"Track: {track['artist']} - {track['title']}")
logger.info(f" Video With Vocals: {track['video_with_vocals']}")
logger.info(f" Video With Instrumental: {track['video_with_instrumental']}")
logger.info(f" Final Video with Title Screen: {track['final_video']}")
logger.info(f" Final Video with Title: {track['final_video']}")


if __name__ == "__main__":
Expand Down
10 changes: 7 additions & 3 deletions utils/prep_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import argparse
import logging
import pkg_resources
import tempfile
import os
from karaoke_prep import KaraokePrep


Expand Down Expand Up @@ -43,9 +45,11 @@ def main():
help="Optional: model name to be used for separation (default: %(default)s). Example: --model_name=UVR-MDX-NET-Inst_HQ_3",
)

# Use tempfile to get the platform-independent temp directory
default_model_dir = os.path.join(tempfile.gettempdir(), "audio-separator-models")
parser.add_argument(
"--model_file_dir",
default="/tmp/audio-separator-models/",
default=default_model_dir,
help="Optional: model files directory (default: %(default)s). Example: --model_file_dir=/app/models",
)

Expand All @@ -57,8 +61,8 @@ def main():

parser.add_argument(
"--output_format",
default="MP3",
help="Optional: output format for separated audio (default: MP3). Example: --output_format=FLAC",
default="mp3",
help="Optional: output format for separated audio (default: mp3). Example: --output_format=flac",
)

parser.add_argument(
Expand Down

0 comments on commit 8924875

Please sign in to comment.