karaoke-gen 0.50.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of karaoke-gen might be problematic. Click here for more details.
- karaoke_gen-0.50.0.dist-info/LICENSE +21 -0
- karaoke_gen-0.50.0.dist-info/METADATA +140 -0
- karaoke_gen-0.50.0.dist-info/RECORD +23 -0
- karaoke_gen-0.50.0.dist-info/WHEEL +4 -0
- karaoke_gen-0.50.0.dist-info/entry_points.txt +4 -0
- karaoke_prep/__init__.py +1 -0
- karaoke_prep/audio_processor.py +396 -0
- karaoke_prep/config.py +134 -0
- karaoke_prep/file_handler.py +186 -0
- karaoke_prep/karaoke_finalise/__init__.py +1 -0
- karaoke_prep/karaoke_finalise/karaoke_finalise.py +1163 -0
- karaoke_prep/karaoke_prep.py +687 -0
- karaoke_prep/lyrics_processor.py +225 -0
- karaoke_prep/metadata.py +105 -0
- karaoke_prep/resources/AvenirNext-Bold.ttf +0 -0
- karaoke_prep/resources/Montserrat-Bold.ttf +0 -0
- karaoke_prep/resources/Oswald-Bold.ttf +0 -0
- karaoke_prep/resources/Oswald-SemiBold.ttf +0 -0
- karaoke_prep/resources/Zurich_Cn_BT_Bold.ttf +0 -0
- karaoke_prep/utils/__init__.py +18 -0
- karaoke_prep/utils/bulk_cli.py +483 -0
- karaoke_prep/utils/gen_cli.py +873 -0
- karaoke_prep/video_generator.py +424 -0
karaoke_prep/config.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
# Default style parameters if no JSON file is provided or if it's invalid
|
|
7
|
+
DEFAULT_STYLE_PARAMS = {
|
|
8
|
+
"intro": {
|
|
9
|
+
"video_duration": 5,
|
|
10
|
+
"existing_image": None,
|
|
11
|
+
"background_color": "#000000",
|
|
12
|
+
"background_image": None,
|
|
13
|
+
"font": "Montserrat-Bold.ttf",
|
|
14
|
+
"artist_color": "#ffdf6b",
|
|
15
|
+
"artist_gradient": None,
|
|
16
|
+
"title_color": "#ffffff",
|
|
17
|
+
"title_gradient": None,
|
|
18
|
+
"title_region": "370, 200, 3100, 480",
|
|
19
|
+
"artist_region": "370, 700, 3100, 480",
|
|
20
|
+
"extra_text": None,
|
|
21
|
+
"extra_text_color": "#ffffff",
|
|
22
|
+
"extra_text_gradient": None,
|
|
23
|
+
"extra_text_region": "370, 1200, 3100, 480",
|
|
24
|
+
"title_text_transform": None, # none, uppercase, lowercase, propercase
|
|
25
|
+
"artist_text_transform": None, # none, uppercase, lowercase, propercase
|
|
26
|
+
},
|
|
27
|
+
"end": {
|
|
28
|
+
"video_duration": 5,
|
|
29
|
+
"existing_image": None,
|
|
30
|
+
"background_color": "#000000",
|
|
31
|
+
"background_image": None,
|
|
32
|
+
"font": "Montserrat-Bold.ttf",
|
|
33
|
+
"artist_color": "#ffdf6b",
|
|
34
|
+
"artist_gradient": None,
|
|
35
|
+
"title_color": "#ffffff",
|
|
36
|
+
"title_gradient": None,
|
|
37
|
+
"title_region": None,
|
|
38
|
+
"artist_region": None,
|
|
39
|
+
"extra_text": "THANK YOU FOR SINGING!",
|
|
40
|
+
"extra_text_color": "#ff7acc",
|
|
41
|
+
"extra_text_gradient": None,
|
|
42
|
+
"extra_text_region": None,
|
|
43
|
+
"title_text_transform": None, # none, uppercase, lowercase, propercase
|
|
44
|
+
"artist_text_transform": None, # none, uppercase, lowercase, propercase
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_style_params(style_params_json, logger):
|
|
50
|
+
"""Loads style parameters from a JSON file or uses defaults."""
|
|
51
|
+
if style_params_json:
|
|
52
|
+
try:
|
|
53
|
+
with open(style_params_json, "r") as f:
|
|
54
|
+
style_params = json.loads(f.read())
|
|
55
|
+
logger.info(f"Loaded style parameters from {style_params_json}")
|
|
56
|
+
# You might want to add validation here to ensure the structure matches expectations
|
|
57
|
+
return style_params
|
|
58
|
+
except FileNotFoundError:
|
|
59
|
+
logger.error(f"Style parameters configuration file not found: {style_params_json}")
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
except json.JSONDecodeError as e:
|
|
62
|
+
logger.error(f"Invalid JSON in style parameters configuration file: {e}")
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"Error loading style parameters file {style_params_json}: {e}")
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
else:
|
|
68
|
+
logger.info("No style parameters JSON file provided. Using default styles.")
|
|
69
|
+
return DEFAULT_STYLE_PARAMS
|
|
70
|
+
|
|
71
|
+
def setup_title_format(style_params):
|
|
72
|
+
"""Sets up the title format dictionary from style parameters."""
|
|
73
|
+
intro_params = style_params.get("intro", DEFAULT_STYLE_PARAMS["intro"])
|
|
74
|
+
return {
|
|
75
|
+
"background_color": intro_params.get("background_color", DEFAULT_STYLE_PARAMS["intro"]["background_color"]),
|
|
76
|
+
"background_image": intro_params.get("background_image"),
|
|
77
|
+
"font": intro_params.get("font", DEFAULT_STYLE_PARAMS["intro"]["font"]),
|
|
78
|
+
"artist_color": intro_params.get("artist_color", DEFAULT_STYLE_PARAMS["intro"]["artist_color"]),
|
|
79
|
+
"artist_gradient": intro_params.get("artist_gradient"),
|
|
80
|
+
"title_color": intro_params.get("title_color", DEFAULT_STYLE_PARAMS["intro"]["title_color"]),
|
|
81
|
+
"title_gradient": intro_params.get("title_gradient"),
|
|
82
|
+
"extra_text": intro_params.get("extra_text"),
|
|
83
|
+
"extra_text_color": intro_params.get("extra_text_color", DEFAULT_STYLE_PARAMS["intro"]["extra_text_color"]),
|
|
84
|
+
"extra_text_gradient": intro_params.get("extra_text_gradient"),
|
|
85
|
+
"extra_text_region": intro_params.get("extra_text_region", DEFAULT_STYLE_PARAMS["intro"]["extra_text_region"]),
|
|
86
|
+
"title_region": intro_params.get("title_region", DEFAULT_STYLE_PARAMS["intro"]["title_region"]),
|
|
87
|
+
"artist_region": intro_params.get("artist_region", DEFAULT_STYLE_PARAMS["intro"]["artist_region"]),
|
|
88
|
+
"title_text_transform": intro_params.get("title_text_transform"),
|
|
89
|
+
"artist_text_transform": intro_params.get("artist_text_transform"),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
def setup_end_format(style_params):
|
|
93
|
+
"""Sets up the end format dictionary from style parameters."""
|
|
94
|
+
end_params = style_params.get("end", DEFAULT_STYLE_PARAMS["end"])
|
|
95
|
+
return {
|
|
96
|
+
"background_color": end_params.get("background_color", DEFAULT_STYLE_PARAMS["end"]["background_color"]),
|
|
97
|
+
"background_image": end_params.get("background_image"),
|
|
98
|
+
"font": end_params.get("font", DEFAULT_STYLE_PARAMS["end"]["font"]),
|
|
99
|
+
"artist_color": end_params.get("artist_color", DEFAULT_STYLE_PARAMS["end"]["artist_color"]),
|
|
100
|
+
"artist_gradient": end_params.get("artist_gradient"),
|
|
101
|
+
"title_color": end_params.get("title_color", DEFAULT_STYLE_PARAMS["end"]["title_color"]),
|
|
102
|
+
"title_gradient": end_params.get("title_gradient"),
|
|
103
|
+
"extra_text": end_params.get("extra_text", DEFAULT_STYLE_PARAMS["end"]["extra_text"]),
|
|
104
|
+
"extra_text_color": end_params.get("extra_text_color", DEFAULT_STYLE_PARAMS["end"]["extra_text_color"]),
|
|
105
|
+
"extra_text_gradient": end_params.get("extra_text_gradient"),
|
|
106
|
+
"extra_text_region": end_params.get("extra_text_region"),
|
|
107
|
+
"title_region": end_params.get("title_region"),
|
|
108
|
+
"artist_region": end_params.get("artist_region"),
|
|
109
|
+
"title_text_transform": end_params.get("title_text_transform"),
|
|
110
|
+
"artist_text_transform": end_params.get("artist_text_transform"),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
def get_video_durations(style_params):
|
|
114
|
+
"""Gets intro and end video durations from style parameters."""
|
|
115
|
+
intro_duration = style_params.get("intro", {}).get("video_duration", DEFAULT_STYLE_PARAMS["intro"]["video_duration"])
|
|
116
|
+
end_duration = style_params.get("end", {}).get("video_duration", DEFAULT_STYLE_PARAMS["end"]["video_duration"])
|
|
117
|
+
return intro_duration, end_duration
|
|
118
|
+
|
|
119
|
+
def get_existing_images(style_params):
|
|
120
|
+
"""Gets existing title and end images from style parameters."""
|
|
121
|
+
existing_title_image = style_params.get("intro", {}).get("existing_image")
|
|
122
|
+
existing_end_image = style_params.get("end", {}).get("existing_image")
|
|
123
|
+
return existing_title_image, existing_end_image
|
|
124
|
+
|
|
125
|
+
def setup_ffmpeg_command(log_level):
|
|
126
|
+
"""Sets up the base ffmpeg command string based on log level."""
|
|
127
|
+
# Path to the Windows PyInstaller frozen bundled ffmpeg.exe, or the system-installed FFmpeg binary on Mac/Linux
|
|
128
|
+
ffmpeg_path = os.path.join(sys._MEIPASS, "ffmpeg.exe") if getattr(sys, "frozen", False) else "ffmpeg"
|
|
129
|
+
ffmpeg_base_command = f"{ffmpeg_path} -hide_banner -nostats"
|
|
130
|
+
if log_level == logging.DEBUG:
|
|
131
|
+
ffmpeg_base_command += " -loglevel verbose"
|
|
132
|
+
else:
|
|
133
|
+
ffmpeg_base_command += " -loglevel fatal"
|
|
134
|
+
return ffmpeg_base_command
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import glob
|
|
3
|
+
import logging
|
|
4
|
+
import shutil
|
|
5
|
+
import tempfile
|
|
6
|
+
import yt_dlp.YoutubeDL as ydl
|
|
7
|
+
from .utils import sanitize_filename
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Placeholder class or functions for file handling
|
|
11
|
+
class FileHandler:
|
|
12
|
+
def __init__(self, logger, ffmpeg_base_command, create_track_subfolders, dry_run):
|
|
13
|
+
self.logger = logger
|
|
14
|
+
self.ffmpeg_base_command = ffmpeg_base_command
|
|
15
|
+
self.create_track_subfolders = create_track_subfolders
|
|
16
|
+
self.dry_run = dry_run
|
|
17
|
+
|
|
18
|
+
def _file_exists(self, file_path):
|
|
19
|
+
"""Check if a file exists and log the result."""
|
|
20
|
+
exists = os.path.isfile(file_path)
|
|
21
|
+
if exists:
|
|
22
|
+
self.logger.info(f"File already exists, skipping creation: {file_path}")
|
|
23
|
+
return exists
|
|
24
|
+
|
|
25
|
+
# Placeholder methods - to be filled by user moving code
|
|
26
|
+
def copy_input_media(self, input_media, output_filename_no_extension):
|
|
27
|
+
self.logger.debug(f"Copying media from local path {input_media} to filename {output_filename_no_extension} + existing extension")
|
|
28
|
+
|
|
29
|
+
copied_file_name = output_filename_no_extension + os.path.splitext(input_media)[1]
|
|
30
|
+
self.logger.debug(f"Target filename: {copied_file_name}")
|
|
31
|
+
|
|
32
|
+
# Check if source and destination are the same
|
|
33
|
+
if os.path.abspath(input_media) == os.path.abspath(copied_file_name):
|
|
34
|
+
self.logger.info("Source and destination are the same file, skipping copy")
|
|
35
|
+
return input_media
|
|
36
|
+
|
|
37
|
+
self.logger.debug(f"Copying {input_media} to {copied_file_name}")
|
|
38
|
+
shutil.copy2(input_media, copied_file_name)
|
|
39
|
+
|
|
40
|
+
return copied_file_name
|
|
41
|
+
|
|
42
|
+
def download_video(self, url, output_filename_no_extension):
|
|
43
|
+
self.logger.debug(f"Downloading media from URL {url} to filename {output_filename_no_extension} + (as yet) unknown extension")
|
|
44
|
+
|
|
45
|
+
ydl_opts = {
|
|
46
|
+
"quiet": True,
|
|
47
|
+
"format": "bv*+ba/b", # if a combined video + audio format is better than the best video-only format use the combined format
|
|
48
|
+
"outtmpl": f"{output_filename_no_extension}.%(ext)s",
|
|
49
|
+
"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",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
with ydl(ydl_opts) as ydl_instance:
|
|
53
|
+
ydl_instance.download([url])
|
|
54
|
+
|
|
55
|
+
# Search for the file with any extension
|
|
56
|
+
downloaded_files = glob.glob(f"{output_filename_no_extension}.*")
|
|
57
|
+
if downloaded_files:
|
|
58
|
+
downloaded_file_name = downloaded_files[0] # Assume the first match is the correct one
|
|
59
|
+
self.logger.info(f"Download finished, returning downloaded filename: {downloaded_file_name}")
|
|
60
|
+
return downloaded_file_name
|
|
61
|
+
else:
|
|
62
|
+
self.logger.error("No files found matching the download pattern.")
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def extract_still_image_from_video(self, input_filename, output_filename_no_extension):
|
|
66
|
+
output_filename = output_filename_no_extension + ".png"
|
|
67
|
+
self.logger.info(f"Extracting still image from position 30s input media")
|
|
68
|
+
ffmpeg_command = f'{self.ffmpeg_base_command} -i "{input_filename}" -ss 00:00:30 -vframes 1 "{output_filename}"'
|
|
69
|
+
self.logger.debug(f"Running command: {ffmpeg_command}")
|
|
70
|
+
os.system(ffmpeg_command)
|
|
71
|
+
return output_filename
|
|
72
|
+
|
|
73
|
+
def convert_to_wav(self, input_filename, output_filename_no_extension):
|
|
74
|
+
"""Convert input audio to WAV format, with input validation."""
|
|
75
|
+
# Validate input file exists and is readable
|
|
76
|
+
if not os.path.isfile(input_filename):
|
|
77
|
+
raise Exception(f"Input audio file not found: {input_filename}")
|
|
78
|
+
|
|
79
|
+
if os.path.getsize(input_filename) == 0:
|
|
80
|
+
raise Exception(f"Input audio file is empty: {input_filename}")
|
|
81
|
+
|
|
82
|
+
# Validate input file format using ffprobe
|
|
83
|
+
probe_command = f'ffprobe -v error -show_entries stream=codec_type -of default=noprint_wrappers=1 "{input_filename}"'
|
|
84
|
+
probe_output = os.popen(probe_command).read()
|
|
85
|
+
|
|
86
|
+
if "codec_type=audio" not in probe_output:
|
|
87
|
+
raise Exception(f"No valid audio stream found in file: {input_filename}")
|
|
88
|
+
|
|
89
|
+
output_filename = output_filename_no_extension + ".wav"
|
|
90
|
+
self.logger.info(f"Converting input media to audio WAV file")
|
|
91
|
+
ffmpeg_command = f'{self.ffmpeg_base_command} -n -i "{input_filename}" "{output_filename}"'
|
|
92
|
+
self.logger.debug(f"Running command: {ffmpeg_command}")
|
|
93
|
+
if not self.dry_run:
|
|
94
|
+
os.system(ffmpeg_command)
|
|
95
|
+
return output_filename
|
|
96
|
+
|
|
97
|
+
def setup_output_paths(self, output_dir, artist, title):
|
|
98
|
+
if title is None and artist is None:
|
|
99
|
+
raise ValueError("Error: At least title or artist must be provided")
|
|
100
|
+
|
|
101
|
+
# If only title is provided, use it for both artist and title portions of paths
|
|
102
|
+
if artist is None:
|
|
103
|
+
sanitized_title = sanitize_filename(title)
|
|
104
|
+
artist_title = sanitized_title
|
|
105
|
+
else:
|
|
106
|
+
sanitized_artist = sanitize_filename(artist)
|
|
107
|
+
sanitized_title = sanitize_filename(title)
|
|
108
|
+
artist_title = f"{sanitized_artist} - {sanitized_title}"
|
|
109
|
+
|
|
110
|
+
track_output_dir = output_dir
|
|
111
|
+
if self.create_track_subfolders:
|
|
112
|
+
track_output_dir = os.path.join(output_dir, f"{artist_title}")
|
|
113
|
+
|
|
114
|
+
if not os.path.exists(track_output_dir):
|
|
115
|
+
self.logger.debug(f"Output dir {track_output_dir} did not exist, creating")
|
|
116
|
+
os.makedirs(track_output_dir)
|
|
117
|
+
|
|
118
|
+
return track_output_dir, artist_title
|
|
119
|
+
|
|
120
|
+
def backup_existing_outputs(self, track_output_dir, artist, title):
|
|
121
|
+
"""
|
|
122
|
+
Backup existing outputs to a versioned folder.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
track_output_dir: The directory containing the track outputs
|
|
126
|
+
artist: The artist name
|
|
127
|
+
title: The track title
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
The path to the original input audio file
|
|
131
|
+
"""
|
|
132
|
+
self.logger.info(f"Backing up existing outputs for {artist} - {title}")
|
|
133
|
+
|
|
134
|
+
# Sanitize artist and title for filenames
|
|
135
|
+
sanitized_artist = sanitize_filename(artist)
|
|
136
|
+
sanitized_title = sanitize_filename(title)
|
|
137
|
+
base_name = f"{sanitized_artist} - {sanitized_title}"
|
|
138
|
+
|
|
139
|
+
# Find the next available version number
|
|
140
|
+
version_num = 1
|
|
141
|
+
while os.path.exists(os.path.join(track_output_dir, f"version-{version_num}")):
|
|
142
|
+
version_num += 1
|
|
143
|
+
|
|
144
|
+
version_dir = os.path.join(track_output_dir, f"version-{version_num}")
|
|
145
|
+
self.logger.info(f"Creating backup directory: {version_dir}")
|
|
146
|
+
os.makedirs(version_dir, exist_ok=True)
|
|
147
|
+
|
|
148
|
+
# Find the input audio file (we'll need this for re-running the transcription)
|
|
149
|
+
input_audio_wav = os.path.join(track_output_dir, f"{base_name}.wav")
|
|
150
|
+
if not os.path.exists(input_audio_wav):
|
|
151
|
+
self.logger.warning(f"Input audio file not found: {input_audio_wav}")
|
|
152
|
+
# Try to find any WAV file
|
|
153
|
+
wav_files = glob.glob(os.path.join(track_output_dir, "*.wav"))
|
|
154
|
+
if wav_files:
|
|
155
|
+
input_audio_wav = wav_files[0]
|
|
156
|
+
self.logger.info(f"Using alternative input audio file: {input_audio_wav}")
|
|
157
|
+
else:
|
|
158
|
+
raise Exception(f"No input audio file found in {track_output_dir}")
|
|
159
|
+
|
|
160
|
+
# List of file patterns to move
|
|
161
|
+
file_patterns = [
|
|
162
|
+
f"{base_name} (With Vocals).*",
|
|
163
|
+
f"{base_name} (Karaoke).*",
|
|
164
|
+
f"{base_name} (Final Karaoke*).*",
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
# Move files matching patterns to version directory
|
|
168
|
+
for pattern in file_patterns:
|
|
169
|
+
for file_path in glob.glob(os.path.join(track_output_dir, pattern)):
|
|
170
|
+
if os.path.isfile(file_path):
|
|
171
|
+
dest_path = os.path.join(version_dir, os.path.basename(file_path))
|
|
172
|
+
self.logger.info(f"Moving {file_path} to {dest_path}")
|
|
173
|
+
if not self.dry_run:
|
|
174
|
+
shutil.move(file_path, dest_path)
|
|
175
|
+
|
|
176
|
+
# Also backup the lyrics directory
|
|
177
|
+
lyrics_dir = os.path.join(track_output_dir, "lyrics")
|
|
178
|
+
if os.path.exists(lyrics_dir):
|
|
179
|
+
lyrics_backup_dir = os.path.join(version_dir, "lyrics")
|
|
180
|
+
self.logger.info(f"Backing up lyrics directory to {lyrics_backup_dir}")
|
|
181
|
+
if not self.dry_run:
|
|
182
|
+
shutil.copytree(lyrics_dir, lyrics_backup_dir)
|
|
183
|
+
# Remove the original lyrics directory
|
|
184
|
+
shutil.rmtree(lyrics_dir)
|
|
185
|
+
|
|
186
|
+
return input_audio_wav
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .karaoke_finalise import KaraokeFinalise
|