karaoke-gen 0.50.0__py3-none-any.whl → 0.56.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/__init__.py +7 -0
- {karaoke_prep → karaoke_gen}/audio_processor.py +21 -16
- {karaoke_prep → karaoke_gen}/file_handler.py +50 -14
- {karaoke_prep → karaoke_gen}/karaoke_finalise/karaoke_finalise.py +15 -2
- karaoke_prep/karaoke_prep.py → karaoke_gen/karaoke_gen.py +23 -10
- {karaoke_prep → karaoke_gen}/lyrics_processor.py +72 -12
- {karaoke_prep → karaoke_gen}/metadata.py +71 -21
- {karaoke_prep → karaoke_gen}/utils/bulk_cli.py +2 -2
- {karaoke_prep → karaoke_gen}/utils/gen_cli.py +32 -94
- {karaoke_prep → karaoke_gen}/video_generator.py +5 -5
- {karaoke_gen-0.50.0.dist-info → karaoke_gen-0.56.0.dist-info}/METADATA +39 -12
- karaoke_gen-0.56.0.dist-info/RECORD +23 -0
- karaoke_gen-0.56.0.dist-info/entry_points.txt +4 -0
- karaoke_gen-0.50.0.dist-info/RECORD +0 -23
- karaoke_gen-0.50.0.dist-info/entry_points.txt +0 -4
- karaoke_prep/__init__.py +0 -1
- {karaoke_prep → karaoke_gen}/config.py +0 -0
- {karaoke_prep → karaoke_gen}/karaoke_finalise/__init__.py +0 -0
- {karaoke_prep → karaoke_gen}/resources/AvenirNext-Bold.ttf +0 -0
- {karaoke_prep → karaoke_gen}/resources/Montserrat-Bold.ttf +0 -0
- {karaoke_prep → karaoke_gen}/resources/Oswald-Bold.ttf +0 -0
- {karaoke_prep → karaoke_gen}/resources/Oswald-SemiBold.ttf +0 -0
- {karaoke_prep → karaoke_gen}/resources/Zurich_Cn_BT_Bold.ttf +0 -0
- {karaoke_prep → karaoke_gen}/utils/__init__.py +0 -0
- {karaoke_gen-0.50.0.dist-info → karaoke_gen-0.56.0.dist-info}/LICENSE +0 -0
- {karaoke_gen-0.50.0.dist-info → karaoke_gen-0.56.0.dist-info}/WHEEL +0 -0
karaoke_gen/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
# Suppress specific SyntaxWarnings from third-party packages
|
|
4
|
+
warnings.filterwarnings("ignore", category=SyntaxWarning, module="pydub.*")
|
|
5
|
+
warnings.filterwarnings("ignore", category=SyntaxWarning, module="syrics.*")
|
|
6
|
+
|
|
7
|
+
from .karaoke_gen import KaraokePrep
|
|
@@ -72,11 +72,11 @@ class AudioProcessor:
|
|
|
72
72
|
|
|
73
73
|
for file in output_files:
|
|
74
74
|
if "(Vocals)" in file:
|
|
75
|
-
self.logger.info(f"
|
|
76
|
-
|
|
75
|
+
self.logger.info(f"Moving Vocals file {file} to {vocals_path}")
|
|
76
|
+
shutil.move(file, vocals_path)
|
|
77
77
|
elif "(Instrumental)" in file:
|
|
78
|
-
self.logger.info(f"
|
|
79
|
-
|
|
78
|
+
self.logger.info(f"Moving Instrumental file {file} to {instrumental_path}")
|
|
79
|
+
shutil.move(file, instrumental_path)
|
|
80
80
|
elif model_name in file:
|
|
81
81
|
# Example filename 1: "Freddie Jackson - All I'll Ever Ask (feat. Najee) (Local)_(Piano)_htdemucs_6s.flac"
|
|
82
82
|
# Example filename 2: "Freddie Jackson - All I'll Ever Ask (feat. Najee) (Local)_(Guitar)_htdemucs_6s.flac"
|
|
@@ -86,8 +86,8 @@ class AudioProcessor:
|
|
|
86
86
|
stem_name = stem_name.strip("()") # Remove parentheses if present
|
|
87
87
|
|
|
88
88
|
other_stem_path = os.path.join(track_output_dir, f"{artist_title} ({stem_name} {model_name}).{self.lossless_output_format}")
|
|
89
|
-
self.logger.info(f"
|
|
90
|
-
|
|
89
|
+
self.logger.info(f"Moving other stem file {file} to {other_stem_path}")
|
|
90
|
+
shutil.move(file, other_stem_path)
|
|
91
91
|
|
|
92
92
|
elif model_name_no_extension in file:
|
|
93
93
|
# Example filename 1: "Freddie Jackson - All I'll Ever Ask (feat. Najee) (Local)_(Piano)_htdemucs_6s.flac"
|
|
@@ -98,8 +98,8 @@ class AudioProcessor:
|
|
|
98
98
|
stem_name = stem_name.strip("()") # Remove parentheses if present
|
|
99
99
|
|
|
100
100
|
other_stem_path = os.path.join(track_output_dir, f"{artist_title} ({stem_name} {model_name}).{self.lossless_output_format}")
|
|
101
|
-
self.logger.info(f"
|
|
102
|
-
|
|
101
|
+
self.logger.info(f"Moving other stem file {file} to {other_stem_path}")
|
|
102
|
+
shutil.move(file, other_stem_path)
|
|
103
103
|
|
|
104
104
|
self.logger.info(f"Separation complete! Output file(s): {vocals_path} {instrumental_path}")
|
|
105
105
|
|
|
@@ -133,8 +133,13 @@ class AudioProcessor:
|
|
|
133
133
|
runtime_mins = runtime.total_seconds() / 60
|
|
134
134
|
|
|
135
135
|
# Get process command line
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
try:
|
|
137
|
+
proc = psutil.Process(pid)
|
|
138
|
+
cmdline_args = proc.cmdline()
|
|
139
|
+
# Handle potential bytes in cmdline args (cross-platform compatibility)
|
|
140
|
+
cmd = " ".join(arg.decode('utf-8', errors='replace') if isinstance(arg, bytes) else arg for arg in cmdline_args)
|
|
141
|
+
except (psutil.AccessDenied, psutil.NoSuchProcess):
|
|
142
|
+
cmd = "<command unavailable>"
|
|
138
143
|
|
|
139
144
|
self.logger.info(
|
|
140
145
|
f"Waiting for other audio separation process to complete before starting separation for {artist_title}...\n"
|
|
@@ -182,7 +187,7 @@ class AudioProcessor:
|
|
|
182
187
|
stems_dir = self._create_stems_directory(track_output_dir)
|
|
183
188
|
result = {"clean_instrumental": {}, "other_stems": {}, "backing_vocals": {}, "combined_instrumentals": {}}
|
|
184
189
|
|
|
185
|
-
if os.environ.get("
|
|
190
|
+
if os.environ.get("KARAOKE_GEN_SKIP_AUDIO_SEPARATION"):
|
|
186
191
|
return result
|
|
187
192
|
|
|
188
193
|
result["clean_instrumental"] = self._separate_clean_instrumental(
|
|
@@ -257,10 +262,10 @@ class AudioProcessor:
|
|
|
257
262
|
|
|
258
263
|
for file in clean_output_files:
|
|
259
264
|
if "(Vocals)" in file and not self._file_exists(vocals_path):
|
|
260
|
-
|
|
265
|
+
shutil.move(file, vocals_path)
|
|
261
266
|
result["vocals"] = vocals_path
|
|
262
267
|
elif "(Instrumental)" in file and not self._file_exists(instrumental_path):
|
|
263
|
-
|
|
268
|
+
shutil.move(file, instrumental_path)
|
|
264
269
|
result["instrumental"] = instrumental_path
|
|
265
270
|
else:
|
|
266
271
|
result["vocals"] = vocals_path
|
|
@@ -293,7 +298,7 @@ class AudioProcessor:
|
|
|
293
298
|
new_filename = f"{artist_title} ({stem_name} {model}).{self.lossless_output_format}"
|
|
294
299
|
other_stem_path = os.path.join(stems_dir, new_filename)
|
|
295
300
|
if not self._file_exists(other_stem_path):
|
|
296
|
-
|
|
301
|
+
shutil.move(file, other_stem_path)
|
|
297
302
|
result[model][stem_name] = other_stem_path
|
|
298
303
|
|
|
299
304
|
return result
|
|
@@ -313,10 +318,10 @@ class AudioProcessor:
|
|
|
313
318
|
|
|
314
319
|
for file in backing_vocals_output:
|
|
315
320
|
if "(Vocals)" in file and not self._file_exists(lead_vocals_path):
|
|
316
|
-
|
|
321
|
+
shutil.move(file, lead_vocals_path)
|
|
317
322
|
result[model]["lead_vocals"] = lead_vocals_path
|
|
318
323
|
elif "(Instrumental)" in file and not self._file_exists(backing_vocals_path):
|
|
319
|
-
|
|
324
|
+
shutil.move(file, backing_vocals_path)
|
|
320
325
|
result[model]["backing_vocals"] = backing_vocals_path
|
|
321
326
|
else:
|
|
322
327
|
result[model]["lead_vocals"] = lead_vocals_path
|
|
@@ -39,28 +39,64 @@ class FileHandler:
|
|
|
39
39
|
|
|
40
40
|
return copied_file_name
|
|
41
41
|
|
|
42
|
-
def download_video(self, url, output_filename_no_extension):
|
|
42
|
+
def download_video(self, url, output_filename_no_extension, cookies_str=None):
|
|
43
43
|
self.logger.debug(f"Downloading media from URL {url} to filename {output_filename_no_extension} + (as yet) unknown extension")
|
|
44
44
|
|
|
45
45
|
ydl_opts = {
|
|
46
46
|
"quiet": True,
|
|
47
47
|
"format": "bv*+ba/b", # if a combined video + audio format is better than the best video-only format use the combined format
|
|
48
48
|
"outtmpl": f"{output_filename_no_extension}.%(ext)s",
|
|
49
|
-
|
|
49
|
+
# Enhanced anti-detection options
|
|
50
|
+
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
51
|
+
"referer": "https://www.youtube.com/",
|
|
52
|
+
"sleep_interval": 1,
|
|
53
|
+
"max_sleep_interval": 3,
|
|
54
|
+
"fragment_retries": 3,
|
|
55
|
+
"extractor_retries": 3,
|
|
56
|
+
"retries": 3,
|
|
57
|
+
# Headers to appear more human
|
|
58
|
+
"http_headers": {
|
|
59
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
60
|
+
"Accept-Language": "en-us,en;q=0.5",
|
|
61
|
+
"Accept-Encoding": "gzip, deflate",
|
|
62
|
+
"DNT": "1",
|
|
63
|
+
"Connection": "keep-alive",
|
|
64
|
+
"Upgrade-Insecure-Requests": "1",
|
|
65
|
+
},
|
|
50
66
|
}
|
|
51
67
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
# Add cookies if provided
|
|
69
|
+
if cookies_str:
|
|
70
|
+
self.logger.info("Using provided cookies for enhanced YouTube download access")
|
|
71
|
+
# Save cookies to a temporary file
|
|
72
|
+
import tempfile
|
|
73
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
|
74
|
+
f.write(cookies_str)
|
|
75
|
+
ydl_opts['cookiefile'] = f.name
|
|
76
|
+
else:
|
|
77
|
+
self.logger.info("No cookies provided for download - attempting standard download")
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
with ydl(ydl_opts) as ydl_instance:
|
|
81
|
+
ydl_instance.download([url])
|
|
82
|
+
|
|
83
|
+
# Search for the file with any extension
|
|
84
|
+
downloaded_files = glob.glob(f"{output_filename_no_extension}.*")
|
|
85
|
+
if downloaded_files:
|
|
86
|
+
downloaded_file_name = downloaded_files[0] # Assume the first match is the correct one
|
|
87
|
+
self.logger.info(f"Download finished, returning downloaded filename: {downloaded_file_name}")
|
|
88
|
+
return downloaded_file_name
|
|
89
|
+
else:
|
|
90
|
+
self.logger.error("No files found matching the download pattern.")
|
|
91
|
+
return None
|
|
92
|
+
finally:
|
|
93
|
+
# Clean up temporary cookie file if it was created
|
|
94
|
+
if cookies_str and 'cookiefile' in ydl_opts:
|
|
95
|
+
try:
|
|
96
|
+
import os
|
|
97
|
+
os.unlink(ydl_opts['cookiefile'])
|
|
98
|
+
except:
|
|
99
|
+
pass
|
|
64
100
|
|
|
65
101
|
def extract_still_image_from_video(self, input_filename, output_filename_no_extension):
|
|
66
102
|
output_filename = output_filename_no_extension + ".png"
|
|
@@ -636,7 +636,13 @@ class KaraokeFinalise:
|
|
|
636
636
|
self.logger.info(f"DRY RUN: Would run command: {command}")
|
|
637
637
|
else:
|
|
638
638
|
self.logger.info(f"Running command: {command}")
|
|
639
|
-
os.system(command)
|
|
639
|
+
exit_code = os.system(command)
|
|
640
|
+
|
|
641
|
+
# Check if command failed (non-zero exit code)
|
|
642
|
+
if exit_code != 0:
|
|
643
|
+
error_msg = f"Command failed with exit code {exit_code}: {command}"
|
|
644
|
+
self.logger.error(error_msg)
|
|
645
|
+
raise Exception(error_msg)
|
|
640
646
|
|
|
641
647
|
def remux_with_instrumental(self, with_vocals_file, instrumental_audio, output_file):
|
|
642
648
|
"""Remux the video with instrumental audio to create karaoke version"""
|
|
@@ -924,7 +930,14 @@ class KaraokeFinalise:
|
|
|
924
930
|
else:
|
|
925
931
|
shutil.copy2(output_files["final_karaoke_lossy_mp4"], dest_mp4_file) # Changed to use lossy MP4
|
|
926
932
|
shutil.copy2(output_files["final_karaoke_lossy_720p_mp4"], dest_720p_mp4_file)
|
|
927
|
-
|
|
933
|
+
|
|
934
|
+
# Only copy CDG ZIP if CDG creation is enabled
|
|
935
|
+
if self.enable_cdg and "final_karaoke_cdg_zip" in output_files:
|
|
936
|
+
shutil.copy2(output_files["final_karaoke_cdg_zip"], dest_zip_file)
|
|
937
|
+
self.logger.info(f"Copied CDG ZIP file to public share directory")
|
|
938
|
+
else:
|
|
939
|
+
self.logger.info(f"CDG creation disabled, skipping CDG ZIP copy")
|
|
940
|
+
|
|
928
941
|
self.logger.info(f"Copied final files to public share directory")
|
|
929
942
|
|
|
930
943
|
def sync_public_share_dir_to_rclone_destination(self):
|
|
@@ -69,6 +69,8 @@ class KaraokePrep:
|
|
|
69
69
|
style_params_json=None,
|
|
70
70
|
# Add the new parameter
|
|
71
71
|
skip_separation=False,
|
|
72
|
+
# YouTube/Online Configuration
|
|
73
|
+
cookies_str=None,
|
|
72
74
|
):
|
|
73
75
|
self.log_level = log_level
|
|
74
76
|
self.log_formatter = log_formatter
|
|
@@ -124,6 +126,9 @@ class KaraokePrep:
|
|
|
124
126
|
self.render_bounding_boxes = render_bounding_boxes # Passed to VideoGenerator
|
|
125
127
|
self.style_params_json = style_params_json # Passed to LyricsProcessor
|
|
126
128
|
|
|
129
|
+
# YouTube/Online Config
|
|
130
|
+
self.cookies_str = cookies_str # Passed to metadata extraction and file download
|
|
131
|
+
|
|
127
132
|
# Load style parameters using the config module
|
|
128
133
|
self.style_params = load_style_params(self.style_params_json, self.logger)
|
|
129
134
|
|
|
@@ -197,7 +202,7 @@ class KaraokePrep:
|
|
|
197
202
|
# Compatibility methods for tests - these call the new functions in metadata.py
|
|
198
203
|
def extract_info_for_online_media(self, input_url=None, input_artist=None, input_title=None):
|
|
199
204
|
"""Compatibility method that calls the function in metadata.py"""
|
|
200
|
-
self.extracted_info = extract_info_for_online_media(input_url, input_artist, input_title, self.logger)
|
|
205
|
+
self.extracted_info = extract_info_for_online_media(input_url, input_artist, input_title, self.logger, self.cookies_str)
|
|
201
206
|
return self.extracted_info
|
|
202
207
|
|
|
203
208
|
def parse_single_track_metadata(self, input_artist, input_title):
|
|
@@ -242,7 +247,7 @@ class KaraokePrep:
|
|
|
242
247
|
self.logger.warning(f"Input media '{self.input_media}' is not a file and self.url was not set. Attempting to treat as URL.")
|
|
243
248
|
# This path requires calling extract/parse again, less efficient
|
|
244
249
|
try:
|
|
245
|
-
extracted = extract_info_for_online_media(self.input_media, self.artist, self.title, self.logger)
|
|
250
|
+
extracted = extract_info_for_online_media(self.input_media, self.artist, self.title, self.logger, self.cookies_str)
|
|
246
251
|
if extracted:
|
|
247
252
|
metadata_result = parse_track_metadata(
|
|
248
253
|
extracted, self.artist, self.title, self.persistent_artist, self.logger
|
|
@@ -345,7 +350,7 @@ class KaraokePrep:
|
|
|
345
350
|
|
|
346
351
|
self.logger.info(f"Downloading input media from {self.url}...")
|
|
347
352
|
# Delegate to FileHandler
|
|
348
|
-
processed_track["input_media"] = self.file_handler.download_video(self.url, output_filename_no_extension)
|
|
353
|
+
processed_track["input_media"] = self.file_handler.download_video(self.url, output_filename_no_extension, self.cookies_str)
|
|
349
354
|
|
|
350
355
|
self.logger.info("Extracting still image from downloaded media (if input is video)...")
|
|
351
356
|
# Delegate to FileHandler
|
|
@@ -382,14 +387,20 @@ class KaraokePrep:
|
|
|
382
387
|
# Run transcription in a separate thread
|
|
383
388
|
transcription_future = asyncio.create_task(
|
|
384
389
|
asyncio.to_thread(
|
|
385
|
-
# Delegate to LyricsProcessor
|
|
386
|
-
self.lyrics_processor.transcribe_lyrics,
|
|
390
|
+
# Delegate to LyricsProcessor - pass original artist/title for filenames, lyrics_artist/lyrics_title for processing
|
|
391
|
+
self.lyrics_processor.transcribe_lyrics,
|
|
392
|
+
processed_track["input_audio_wav"],
|
|
393
|
+
self.artist, # Original artist for filename generation
|
|
394
|
+
self.title, # Original title for filename generation
|
|
395
|
+
track_output_dir,
|
|
396
|
+
lyrics_artist, # Lyrics artist for processing
|
|
397
|
+
lyrics_title # Lyrics title for processing
|
|
387
398
|
)
|
|
388
399
|
)
|
|
389
400
|
self.logger.info(f"Transcription future created, type: {type(transcription_future)}")
|
|
390
401
|
|
|
391
|
-
# Default to a placeholder
|
|
392
|
-
separation_future = asyncio.sleep(0)
|
|
402
|
+
# Default to a placeholder task if separation won't run
|
|
403
|
+
separation_future = asyncio.create_task(asyncio.sleep(0))
|
|
393
404
|
|
|
394
405
|
# Only create real separation future if not skipping AND no existing instrumental provided
|
|
395
406
|
if not self.skip_separation and not self.existing_instrumental:
|
|
@@ -501,7 +512,7 @@ class KaraokePrep:
|
|
|
501
512
|
processed_track["title_video"] = os.path.join(track_output_dir, f"{artist_title} (Title).mov")
|
|
502
513
|
|
|
503
514
|
# Use FileHandler._file_exists
|
|
504
|
-
if not self.file_handler._file_exists(processed_track["title_video"]) and not os.environ.get("
|
|
515
|
+
if not self.file_handler._file_exists(processed_track["title_video"]) and not os.environ.get("KARAOKE_GEN_SKIP_TITLE_END_SCREENS"):
|
|
505
516
|
self.logger.info(f"Creating title video...")
|
|
506
517
|
# Delegate to VideoGenerator
|
|
507
518
|
self.video_generator.create_title_video(
|
|
@@ -520,7 +531,7 @@ class KaraokePrep:
|
|
|
520
531
|
processed_track["end_video"] = os.path.join(track_output_dir, f"{artist_title} (End).mov")
|
|
521
532
|
|
|
522
533
|
# Use FileHandler._file_exists
|
|
523
|
-
if not self.file_handler._file_exists(processed_track["end_video"]) and not os.environ.get("
|
|
534
|
+
if not self.file_handler._file_exists(processed_track["end_video"]) and not os.environ.get("KARAOKE_GEN_SKIP_TITLE_END_SCREENS"):
|
|
524
535
|
self.logger.info(f"Creating end screen video...")
|
|
525
536
|
# Delegate to VideoGenerator
|
|
526
537
|
self.video_generator.create_end_video(
|
|
@@ -675,7 +686,7 @@ class KaraokePrep:
|
|
|
675
686
|
self.url = self.input_media
|
|
676
687
|
# Use the imported extract_info_for_online_media function
|
|
677
688
|
self.extracted_info = extract_info_for_online_media(
|
|
678
|
-
input_url=self.url, input_artist=self.artist, input_title=self.title, logger=self.logger
|
|
689
|
+
input_url=self.url, input_artist=self.artist, input_title=self.title, logger=self.logger, cookies_str=self.cookies_str
|
|
679
690
|
)
|
|
680
691
|
|
|
681
692
|
if self.extracted_info and "playlist_count" in self.extracted_info:
|
|
@@ -684,4 +695,6 @@ class KaraokePrep:
|
|
|
684
695
|
return await self.process_playlist()
|
|
685
696
|
else:
|
|
686
697
|
self.logger.info(f"Input URL is not a playlist, processing single track")
|
|
698
|
+
# Parse metadata to extract artist and title before processing
|
|
699
|
+
self.parse_single_track_metadata(self.artist, self.title)
|
|
687
700
|
return [await self.prep_single_track()]
|
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
import re
|
|
3
3
|
import logging
|
|
4
4
|
import shutil
|
|
5
|
+
import json
|
|
5
6
|
from lyrics_transcriber import LyricsTranscriber, OutputConfig, TranscriberConfig, LyricsConfig
|
|
6
7
|
from lyrics_transcriber.core.controller import LyricsControllerResult
|
|
7
8
|
from dotenv import load_dotenv
|
|
@@ -102,14 +103,33 @@ class LyricsProcessor:
|
|
|
102
103
|
|
|
103
104
|
return processed_lines
|
|
104
105
|
|
|
105
|
-
def transcribe_lyrics(self, input_audio_wav, artist, title, track_output_dir):
|
|
106
|
+
def transcribe_lyrics(self, input_audio_wav, artist, title, track_output_dir, lyrics_artist=None, lyrics_title=None):
|
|
107
|
+
"""
|
|
108
|
+
Transcribe lyrics for a track.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
input_audio_wav: Path to the audio file
|
|
112
|
+
artist: Original artist name (used for filename generation)
|
|
113
|
+
title: Original title (used for filename generation)
|
|
114
|
+
track_output_dir: Output directory path
|
|
115
|
+
lyrics_artist: Artist name for lyrics processing (defaults to artist if None)
|
|
116
|
+
lyrics_title: Title for lyrics processing (defaults to title if None)
|
|
117
|
+
"""
|
|
118
|
+
# Use original artist/title for filename generation
|
|
119
|
+
filename_artist = artist
|
|
120
|
+
filename_title = title
|
|
121
|
+
|
|
122
|
+
# Use lyrics_artist/lyrics_title for actual lyrics processing, fall back to originals if not provided
|
|
123
|
+
processing_artist = lyrics_artist or artist
|
|
124
|
+
processing_title = lyrics_title or title
|
|
125
|
+
|
|
106
126
|
self.logger.info(
|
|
107
|
-
f"Transcribing lyrics for track {
|
|
127
|
+
f"Transcribing lyrics for track {processing_artist} - {processing_title} from audio file: {input_audio_wav} with output directory: {track_output_dir}"
|
|
108
128
|
)
|
|
109
129
|
|
|
110
|
-
# Check for existing files first using sanitized names
|
|
111
|
-
sanitized_artist = sanitize_filename(
|
|
112
|
-
sanitized_title = sanitize_filename(
|
|
130
|
+
# Check for existing files first using sanitized names from ORIGINAL artist/title for consistency
|
|
131
|
+
sanitized_artist = sanitize_filename(filename_artist)
|
|
132
|
+
sanitized_title = sanitize_filename(filename_title)
|
|
113
133
|
parent_video_path = os.path.join(track_output_dir, f"{sanitized_artist} - {sanitized_title} (With Vocals).mkv")
|
|
114
134
|
parent_lrc_path = os.path.join(track_output_dir, f"{sanitized_artist} - {sanitized_title} (Karaoke).lrc")
|
|
115
135
|
|
|
@@ -137,10 +157,15 @@ class LyricsProcessor:
|
|
|
137
157
|
"ass_filepath": parent_video_path,
|
|
138
158
|
}
|
|
139
159
|
|
|
140
|
-
# Create lyrics
|
|
160
|
+
# Create lyrics directory if it doesn't exist
|
|
141
161
|
os.makedirs(lyrics_dir, exist_ok=True)
|
|
142
162
|
self.logger.info(f"Created lyrics directory: {lyrics_dir}")
|
|
143
163
|
|
|
164
|
+
# Set render_video to False if explicitly disabled
|
|
165
|
+
render_video = self.render_video
|
|
166
|
+
if not render_video:
|
|
167
|
+
self.logger.info("Video rendering disabled, skipping video output")
|
|
168
|
+
|
|
144
169
|
# Load environment variables
|
|
145
170
|
load_dotenv()
|
|
146
171
|
env_config = {
|
|
@@ -162,29 +187,51 @@ class LyricsProcessor:
|
|
|
162
187
|
lyrics_file=self.lyrics_file,
|
|
163
188
|
)
|
|
164
189
|
|
|
190
|
+
# Detect if we're running in a serverless environment (Modal)
|
|
191
|
+
# Modal sets specific environment variables we can check for
|
|
192
|
+
is_serverless = (
|
|
193
|
+
os.getenv("MODAL_TASK_ID") is not None or
|
|
194
|
+
os.getenv("MODAL_FUNCTION_NAME") is not None or
|
|
195
|
+
os.path.exists("/.modal") # Modal creates this directory in containers
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# In serverless environment, disable interactive review even if skip_transcription_review=False
|
|
199
|
+
# This preserves CLI behavior while fixing serverless hanging
|
|
200
|
+
enable_review_setting = not self.skip_transcription_review and not is_serverless
|
|
201
|
+
|
|
202
|
+
if is_serverless and not self.skip_transcription_review:
|
|
203
|
+
self.logger.info("Detected serverless environment - disabling interactive review to prevent hanging")
|
|
204
|
+
|
|
205
|
+
# In serverless environment, disable video generation during Phase 1 to save compute
|
|
206
|
+
# Video will be generated in Phase 2 after human review
|
|
207
|
+
serverless_render_video = render_video and not is_serverless
|
|
208
|
+
|
|
209
|
+
if is_serverless and render_video:
|
|
210
|
+
self.logger.info("Detected serverless environment - deferring video generation until after review")
|
|
211
|
+
|
|
165
212
|
output_config = OutputConfig(
|
|
166
213
|
output_styles_json=self.style_params_json,
|
|
167
214
|
output_dir=lyrics_dir,
|
|
168
|
-
render_video=
|
|
215
|
+
render_video=serverless_render_video, # Disable video in serverless Phase 1
|
|
169
216
|
fetch_lyrics=True,
|
|
170
217
|
run_transcription=not self.skip_transcription,
|
|
171
218
|
run_correction=True,
|
|
172
219
|
generate_plain_text=True,
|
|
173
220
|
generate_lrc=True,
|
|
174
|
-
generate_cdg=
|
|
221
|
+
generate_cdg=False, # Also defer CDG generation to Phase 2
|
|
175
222
|
video_resolution="4k",
|
|
176
|
-
enable_review=
|
|
223
|
+
enable_review=enable_review_setting,
|
|
177
224
|
subtitle_offset_ms=self.subtitle_offset_ms,
|
|
178
225
|
)
|
|
179
226
|
|
|
180
227
|
# Add this log entry to debug the OutputConfig
|
|
181
228
|
self.logger.info(f"Instantiating LyricsTranscriber with OutputConfig: {output_config}")
|
|
182
229
|
|
|
183
|
-
# Initialize transcriber with new config objects
|
|
230
|
+
# Initialize transcriber with new config objects - use PROCESSING artist/title for lyrics work
|
|
184
231
|
transcriber = LyricsTranscriber(
|
|
185
232
|
audio_filepath=input_audio_wav,
|
|
186
|
-
artist=
|
|
187
|
-
title=
|
|
233
|
+
artist=processing_artist, # Use lyrics_artist for processing
|
|
234
|
+
title=processing_title, # Use lyrics_title for processing
|
|
188
235
|
transcriber_config=transcriber_config,
|
|
189
236
|
lyrics_config=lyrics_config,
|
|
190
237
|
output_config=output_config,
|
|
@@ -216,6 +263,19 @@ class LyricsProcessor:
|
|
|
216
263
|
)
|
|
217
264
|
transcriber_outputs["corrected_lyrics_text_filepath"] = results.corrected_txt
|
|
218
265
|
|
|
266
|
+
# Save correction data to JSON file for review interface
|
|
267
|
+
# Use the expected filename format: "{artist} - {title} (Lyrics Corrections).json"
|
|
268
|
+
corrections_filename = f"{filename_artist} - {filename_title} (Lyrics Corrections).json"
|
|
269
|
+
corrections_filepath = os.path.join(lyrics_dir, corrections_filename)
|
|
270
|
+
|
|
271
|
+
# Use the CorrectionResult's to_dict() method to serialize
|
|
272
|
+
correction_data = results.transcription_corrected.to_dict()
|
|
273
|
+
|
|
274
|
+
with open(corrections_filepath, 'w') as f:
|
|
275
|
+
json.dump(correction_data, f, indent=2)
|
|
276
|
+
|
|
277
|
+
self.logger.info(f"Saved correction data to {corrections_filepath}")
|
|
278
|
+
|
|
219
279
|
if transcriber_outputs:
|
|
220
280
|
self.logger.info(f"*** Transcriber Filepath Outputs: ***")
|
|
221
281
|
for key, value in transcriber_outputs.items():
|
|
@@ -1,30 +1,80 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import yt_dlp.YoutubeDL as ydl
|
|
3
3
|
|
|
4
|
-
def extract_info_for_online_media(input_url, input_artist, input_title, logger):
|
|
4
|
+
def extract_info_for_online_media(input_url, input_artist, input_title, logger, cookies_str=None):
|
|
5
5
|
"""Extracts metadata using yt-dlp, either from a URL or via search."""
|
|
6
6
|
logger.info(f"Extracting info for input_url: {input_url} input_artist: {input_artist} input_title: {input_title}")
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
|
|
8
|
+
# Set up yt-dlp options with enhanced anti-detection
|
|
9
|
+
base_opts = {
|
|
10
|
+
"quiet": True,
|
|
11
|
+
# Anti-detection options
|
|
12
|
+
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
13
|
+
"referer": "https://www.youtube.com/",
|
|
14
|
+
"sleep_interval": 1,
|
|
15
|
+
"max_sleep_interval": 3,
|
|
16
|
+
"fragment_retries": 3,
|
|
17
|
+
"extractor_retries": 3,
|
|
18
|
+
"retries": 3,
|
|
19
|
+
# Headers to appear more human
|
|
20
|
+
"http_headers": {
|
|
21
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
22
|
+
"Accept-Language": "en-us,en;q=0.5",
|
|
23
|
+
"Accept-Encoding": "gzip, deflate",
|
|
24
|
+
"DNT": "1",
|
|
25
|
+
"Connection": "keep-alive",
|
|
26
|
+
"Upgrade-Insecure-Requests": "1",
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Add cookies if provided
|
|
31
|
+
if cookies_str:
|
|
32
|
+
logger.info("Using provided cookies for enhanced YouTube access")
|
|
33
|
+
# Save cookies to a temporary file
|
|
34
|
+
import tempfile
|
|
35
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
|
36
|
+
f.write(cookies_str)
|
|
37
|
+
base_opts['cookiefile'] = f.name
|
|
12
38
|
else:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
39
|
+
logger.info("No cookies provided - attempting standard extraction")
|
|
40
|
+
|
|
41
|
+
extracted_info = None
|
|
42
|
+
try:
|
|
43
|
+
if input_url is not None:
|
|
44
|
+
# If a URL is provided, use it to extract the metadata
|
|
45
|
+
with ydl(base_opts) as ydl_instance:
|
|
46
|
+
extracted_info = ydl_instance.extract_info(input_url, download=False)
|
|
47
|
+
else:
|
|
48
|
+
# If no URL is provided, use the query to search for the top result
|
|
49
|
+
search_opts = base_opts.copy()
|
|
50
|
+
search_opts.update({
|
|
51
|
+
"format": "bestaudio",
|
|
52
|
+
"noplaylist": "True",
|
|
53
|
+
"extract_flat": True
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
with ydl(search_opts) as ydl_instance:
|
|
57
|
+
query = f"{input_artist} {input_title}"
|
|
58
|
+
search_results = ydl_instance.extract_info(f"ytsearch1:{query}", download=False)
|
|
59
|
+
if search_results and "entries" in search_results and search_results["entries"]:
|
|
60
|
+
extracted_info = search_results["entries"][0]
|
|
61
|
+
else:
|
|
62
|
+
# Raise IndexError to match the expected exception in tests
|
|
63
|
+
raise IndexError(f"No search results found on YouTube for query: {input_artist} {input_title}")
|
|
64
|
+
|
|
65
|
+
if not extracted_info:
|
|
66
|
+
raise Exception(f"Failed to extract info for query: {input_artist} {input_title} or URL: {input_url}")
|
|
67
|
+
|
|
68
|
+
return extracted_info
|
|
69
|
+
|
|
70
|
+
finally:
|
|
71
|
+
# Clean up temporary cookie file if it was created
|
|
72
|
+
if cookies_str and 'cookiefile' in base_opts:
|
|
73
|
+
try:
|
|
74
|
+
import os
|
|
75
|
+
os.unlink(base_opts['cookiefile'])
|
|
76
|
+
except:
|
|
77
|
+
pass
|
|
28
78
|
|
|
29
79
|
|
|
30
80
|
def parse_track_metadata(extracted_info, current_artist, current_title, persistent_artist, logger):
|
|
@@ -7,8 +7,8 @@ import csv
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import json
|
|
9
9
|
import sys
|
|
10
|
-
from
|
|
11
|
-
from
|
|
10
|
+
from karaoke_gen import KaraokePrep
|
|
11
|
+
from karaoke_gen.karaoke_finalise import KaraokeFinalise
|
|
12
12
|
|
|
13
13
|
# Global logger
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
|
-
print("DEBUG: gen_cli.py starting imports...")
|
|
3
2
|
import argparse
|
|
4
3
|
import logging
|
|
5
4
|
from importlib import metadata
|
|
@@ -10,10 +9,8 @@ import json
|
|
|
10
9
|
import asyncio
|
|
11
10
|
import time
|
|
12
11
|
import pyperclip
|
|
13
|
-
from
|
|
14
|
-
from
|
|
15
|
-
|
|
16
|
-
print("DEBUG: gen_cli.py imports complete.")
|
|
12
|
+
from karaoke_gen import KaraokePrep
|
|
13
|
+
from karaoke_gen.karaoke_finalise import KaraokeFinalise
|
|
17
14
|
|
|
18
15
|
|
|
19
16
|
def is_url(string):
|
|
@@ -27,15 +24,12 @@ def is_file(string):
|
|
|
27
24
|
|
|
28
25
|
|
|
29
26
|
async def async_main():
|
|
30
|
-
print("DEBUG: async_main() started.")
|
|
31
27
|
logger = logging.getLogger(__name__)
|
|
32
28
|
log_handler = logging.StreamHandler()
|
|
33
29
|
log_formatter = logging.Formatter(fmt="%(asctime)s.%(msecs)03d - %(levelname)s - %(module)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
|
34
30
|
log_handler.setFormatter(log_formatter)
|
|
35
31
|
logger.addHandler(log_handler)
|
|
36
32
|
|
|
37
|
-
print("DEBUG: async_main() logger configured.")
|
|
38
|
-
|
|
39
33
|
parser = argparse.ArgumentParser(
|
|
40
34
|
description="Generate karaoke videos with synchronized lyrics. Handles the entire process from downloading audio and lyrics to creating the final video.",
|
|
41
35
|
formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=54),
|
|
@@ -53,7 +47,6 @@ async def async_main():
|
|
|
53
47
|
package_version = metadata.version("karaoke-gen")
|
|
54
48
|
except metadata.PackageNotFoundError:
|
|
55
49
|
package_version = "unknown"
|
|
56
|
-
print("DEBUG: Could not find version for karaoke-gen")
|
|
57
50
|
|
|
58
51
|
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {package_version}")
|
|
59
52
|
|
|
@@ -290,8 +283,6 @@ async def async_main():
|
|
|
290
283
|
|
|
291
284
|
args = parser.parse_args()
|
|
292
285
|
|
|
293
|
-
print("DEBUG: async_main() args parsed.")
|
|
294
|
-
|
|
295
286
|
# Handle test email template case first
|
|
296
287
|
if args.test_email_template:
|
|
297
288
|
log_level = getattr(logging, args.log_level.upper())
|
|
@@ -305,8 +296,6 @@ async def async_main():
|
|
|
305
296
|
kfinalise.test_email_template()
|
|
306
297
|
return
|
|
307
298
|
|
|
308
|
-
print("DEBUG: async_main() continuing after test_email_template check.")
|
|
309
|
-
|
|
310
299
|
# Handle edit-lyrics mode
|
|
311
300
|
if args.edit_lyrics:
|
|
312
301
|
log_level = getattr(logging, args.log_level.upper())
|
|
@@ -484,8 +473,6 @@ async def async_main():
|
|
|
484
473
|
raise e
|
|
485
474
|
|
|
486
475
|
return
|
|
487
|
-
|
|
488
|
-
print("DEBUG: async_main() continuing after edit_lyrics check.")
|
|
489
476
|
|
|
490
477
|
# Handle finalise-only mode
|
|
491
478
|
if args.finalise_only:
|
|
@@ -512,7 +499,11 @@ async def async_main():
|
|
|
512
499
|
logger.error(f"Invalid JSON in CDG styles configuration file: {e}")
|
|
513
500
|
sys.exit(1)
|
|
514
501
|
return # Explicit return for testing
|
|
515
|
-
|
|
502
|
+
except KeyError:
|
|
503
|
+
logger.error(f"'cdg' key not found in style parameters file: {args.style_params_json}")
|
|
504
|
+
sys.exit(1)
|
|
505
|
+
return # Explicit return for testing
|
|
506
|
+
|
|
516
507
|
kfinalise = KaraokeFinalise(
|
|
517
508
|
log_formatter=log_formatter,
|
|
518
509
|
log_level=log_level,
|
|
@@ -530,13 +521,16 @@ async def async_main():
|
|
|
530
521
|
discord_webhook_url=args.discord_webhook_url,
|
|
531
522
|
email_template_file=args.email_template_file,
|
|
532
523
|
cdg_styles=cdg_styles,
|
|
533
|
-
keep_brand_code=args
|
|
524
|
+
keep_brand_code=getattr(args, 'keep_brand_code', False),
|
|
534
525
|
non_interactive=args.yes,
|
|
535
526
|
)
|
|
536
|
-
|
|
527
|
+
|
|
537
528
|
try:
|
|
538
529
|
track = kfinalise.process()
|
|
539
|
-
logger.info(f"
|
|
530
|
+
logger.info(f"Successfully completed finalisation for: {track['artist']} - {track['title']}")
|
|
531
|
+
|
|
532
|
+
# Display summary of outputs
|
|
533
|
+
logger.info(f"Karaoke finalisation complete! Output files:")
|
|
540
534
|
logger.info(f"")
|
|
541
535
|
logger.info(f"Track: {track['artist']} - {track['title']}")
|
|
542
536
|
logger.info(f"")
|
|
@@ -564,7 +558,7 @@ async def async_main():
|
|
|
564
558
|
logger.info(f"")
|
|
565
559
|
logger.info(f"Organization:")
|
|
566
560
|
logger.info(f" Brand Code: {track['brand_code']}")
|
|
567
|
-
logger.info(f"
|
|
561
|
+
logger.info(f" Directory: {track['new_brand_code_dir_path']}")
|
|
568
562
|
|
|
569
563
|
if track["youtube_url"] or track["brand_code_dir_sharing_link"]:
|
|
570
564
|
logger.info(f"")
|
|
@@ -592,8 +586,6 @@ async def async_main():
|
|
|
592
586
|
|
|
593
587
|
return
|
|
594
588
|
|
|
595
|
-
print("DEBUG: async_main() parsed positional args.")
|
|
596
|
-
|
|
597
589
|
# For prep or full workflow, parse input arguments
|
|
598
590
|
input_media, artist, title, filename_pattern = None, None, None, None
|
|
599
591
|
|
|
@@ -643,23 +635,18 @@ async def async_main():
|
|
|
643
635
|
log_level = getattr(logging, args.log_level.upper())
|
|
644
636
|
logger.setLevel(log_level)
|
|
645
637
|
|
|
646
|
-
print("DEBUG: async_main() log level set.")
|
|
647
|
-
|
|
648
638
|
# Set up environment variables for lyrics-only mode
|
|
649
639
|
if args.lyrics_only:
|
|
650
640
|
args.skip_separation = True
|
|
651
|
-
os.environ["
|
|
652
|
-
os.environ["
|
|
641
|
+
os.environ["KARAOKE_GEN_SKIP_AUDIO_SEPARATION"] = "1"
|
|
642
|
+
os.environ["KARAOKE_GEN_SKIP_TITLE_END_SCREENS"] = "1"
|
|
653
643
|
logger.info("Lyrics-only mode enabled: skipping audio separation and title/end screen generation")
|
|
654
644
|
|
|
655
|
-
print("DEBUG: async_main() instantiating KaraokePrep...")
|
|
656
|
-
|
|
657
645
|
# Step 1: Run KaraokePrep
|
|
658
|
-
logger.info(f"KaraokePrep beginning with input_media: {input_media} artist: {artist} and title: {title}")
|
|
659
646
|
kprep_coroutine = KaraokePrep(
|
|
647
|
+
input_media=input_media,
|
|
660
648
|
artist=artist,
|
|
661
649
|
title=title,
|
|
662
|
-
input_media=input_media,
|
|
663
650
|
filename_pattern=filename_pattern,
|
|
664
651
|
dry_run=args.dry_run,
|
|
665
652
|
log_formatter=log_formatter,
|
|
@@ -688,64 +675,16 @@ async def async_main():
|
|
|
688
675
|
# No await needed for constructor
|
|
689
676
|
kprep = kprep_coroutine
|
|
690
677
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
print(f"DEBUG: kprep type: {type(kprep)}")
|
|
694
|
-
print(f"DEBUG: kprep.process type: {type(kprep.process)}")
|
|
695
|
-
process_coroutine = kprep.process()
|
|
696
|
-
print(f"DEBUG: process_coroutine type: {type(process_coroutine)}")
|
|
697
|
-
tracks = await process_coroutine
|
|
698
|
-
|
|
699
|
-
print("DEBUG: async_main() kprep.process() finished.")
|
|
678
|
+
# Create final tracks data structure
|
|
679
|
+
tracks = await kprep.process()
|
|
700
680
|
|
|
701
|
-
# If prep-only mode,
|
|
681
|
+
# If prep-only mode, we're done
|
|
702
682
|
if args.prep_only:
|
|
703
|
-
logger.info(
|
|
704
|
-
|
|
705
|
-
for track in tracks:
|
|
706
|
-
logger.info(f"")
|
|
707
|
-
logger.info(f"Track: {track['artist']} - {track['title']}")
|
|
708
|
-
logger.info(f" Input Media: {track['input_media']}")
|
|
709
|
-
logger.info(f" Input WAV Audio: {track['input_audio_wav']}")
|
|
710
|
-
logger.info(f" Input Still Image: {track['input_still_image']}")
|
|
711
|
-
logger.info(f" Lyrics: {track['lyrics']}")
|
|
712
|
-
logger.info(f" Processed Lyrics: {track['processed_lyrics']}")
|
|
713
|
-
|
|
714
|
-
logger.info(f" Separated Audio:")
|
|
715
|
-
|
|
716
|
-
# Clean Instrumental
|
|
717
|
-
logger.info(f" Clean Instrumental Model:")
|
|
718
|
-
for stem_type, file_path in track["separated_audio"]["clean_instrumental"].items():
|
|
719
|
-
logger.info(f" {stem_type.capitalize()}: {file_path}")
|
|
720
|
-
|
|
721
|
-
# Other Stems
|
|
722
|
-
logger.info(f" Other Stems Models:")
|
|
723
|
-
for model, stems in track["separated_audio"]["other_stems"].items():
|
|
724
|
-
logger.info(f" Model: {model}")
|
|
725
|
-
for stem_type, file_path in stems.items():
|
|
726
|
-
logger.info(f" {stem_type.capitalize()}: {file_path}")
|
|
727
|
-
|
|
728
|
-
# Backing Vocals
|
|
729
|
-
logger.info(f" Backing Vocals Models:")
|
|
730
|
-
for model, stems in track["separated_audio"]["backing_vocals"].items():
|
|
731
|
-
logger.info(f" Model: {model}")
|
|
732
|
-
for stem_type, file_path in stems.items():
|
|
733
|
-
logger.info(f" {stem_type.capitalize()}: {file_path}")
|
|
734
|
-
|
|
735
|
-
# Combined Instrumentals
|
|
736
|
-
logger.info(f" Combined Instrumentals:")
|
|
737
|
-
for model, file_path in track["separated_audio"]["combined_instrumentals"].items():
|
|
738
|
-
logger.info(f" Model: {model}")
|
|
739
|
-
logger.info(f" Combined Instrumental: {file_path}")
|
|
740
|
-
|
|
741
|
-
logger.info("Preparation phase complete. Exiting due to --prep-only flag.")
|
|
683
|
+
logger.info("Prep-only mode: skipping finalisation phase")
|
|
742
684
|
return
|
|
743
685
|
|
|
744
|
-
print("DEBUG: async_main() continuing after prep_only check.")
|
|
745
|
-
|
|
746
686
|
# Step 2: For each track, run KaraokeFinalise
|
|
747
687
|
for track in tracks:
|
|
748
|
-
print(f"DEBUG: async_main() starting finalise loop for track: {track.get('track_output_dir')}")
|
|
749
688
|
logger.info(f"Starting finalisation phase for {track['artist']} - {track['title']}...")
|
|
750
689
|
|
|
751
690
|
# Use the track directory that was actually created by KaraokePrep
|
|
@@ -776,8 +715,11 @@ async def async_main():
|
|
|
776
715
|
logger.error(f"Invalid JSON in CDG styles configuration file: {e}")
|
|
777
716
|
sys.exit(1)
|
|
778
717
|
return # Explicit return for testing
|
|
718
|
+
except KeyError:
|
|
719
|
+
logger.error(f"'cdg' key not found in style parameters file: {args.style_params_json}")
|
|
720
|
+
sys.exit(1)
|
|
721
|
+
return # Explicit return for testing
|
|
779
722
|
|
|
780
|
-
# Initialize KaraokeFinalise
|
|
781
723
|
kfinalise = KaraokeFinalise(
|
|
782
724
|
log_formatter=log_formatter,
|
|
783
725
|
log_level=log_level,
|
|
@@ -795,16 +737,16 @@ async def async_main():
|
|
|
795
737
|
discord_webhook_url=args.discord_webhook_url,
|
|
796
738
|
email_template_file=args.email_template_file,
|
|
797
739
|
cdg_styles=cdg_styles,
|
|
798
|
-
keep_brand_code=args
|
|
740
|
+
keep_brand_code=getattr(args, 'keep_brand_code', False),
|
|
799
741
|
non_interactive=args.yes,
|
|
800
742
|
)
|
|
801
743
|
|
|
802
744
|
try:
|
|
803
745
|
final_track = kfinalise.process()
|
|
804
|
-
logger.info(f"Successfully completed processing
|
|
746
|
+
logger.info(f"Successfully completed processing: {final_track['artist']} - {final_track['title']}")
|
|
805
747
|
|
|
806
748
|
# Display summary of outputs
|
|
807
|
-
logger.info(f"Karaoke
|
|
749
|
+
logger.info(f"Karaoke generation complete! Output files:")
|
|
808
750
|
logger.info(f"")
|
|
809
751
|
logger.info(f"Track: {final_track['artist']} - {final_track['title']}")
|
|
810
752
|
logger.info(f"")
|
|
@@ -832,7 +774,7 @@ async def async_main():
|
|
|
832
774
|
logger.info(f"")
|
|
833
775
|
logger.info(f"Organization:")
|
|
834
776
|
logger.info(f" Brand Code: {final_track['brand_code']}")
|
|
835
|
-
logger.info(f"
|
|
777
|
+
logger.info(f" Directory: {final_track['new_brand_code_dir_path']}")
|
|
836
778
|
|
|
837
779
|
if final_track["youtube_url"] or final_track["brand_code_dir_sharing_link"]:
|
|
838
780
|
logger.info(f"")
|
|
@@ -854,20 +796,16 @@ async def async_main():
|
|
|
854
796
|
logger.info(f" (YouTube URL copied to clipboard)")
|
|
855
797
|
except Exception as e:
|
|
856
798
|
logger.warning(f" Failed to copy YouTube URL to clipboard: {str(e)}")
|
|
857
|
-
|
|
858
799
|
except Exception as e:
|
|
859
|
-
logger.error(f"
|
|
800
|
+
logger.error(f"An error occurred during finalisation, see stack trace below: {str(e)}")
|
|
860
801
|
raise e
|
|
861
|
-
|
|
862
|
-
|
|
802
|
+
|
|
803
|
+
return
|
|
863
804
|
|
|
864
805
|
|
|
865
806
|
def main():
|
|
866
|
-
print("DEBUG: main() started.")
|
|
867
807
|
asyncio.run(async_main())
|
|
868
|
-
print("DEBUG: main() finished.")
|
|
869
808
|
|
|
870
809
|
|
|
871
810
|
if __name__ == "__main__":
|
|
872
|
-
print("DEBUG: __main__ block executing.")
|
|
873
811
|
main()
|
|
@@ -71,7 +71,7 @@ class VideoGenerator:
|
|
|
71
71
|
else:
|
|
72
72
|
# Try to load from package resources
|
|
73
73
|
try:
|
|
74
|
-
with pkg_resources.path("
|
|
74
|
+
with pkg_resources.path("karaoke_gen.resources", format["font"]) as font_path:
|
|
75
75
|
font_path = str(font_path)
|
|
76
76
|
except Exception as e:
|
|
77
77
|
self.logger.warning(f"Could not load font from resources: {e}, falling back to default font")
|
|
@@ -96,7 +96,7 @@ class VideoGenerator:
|
|
|
96
96
|
|
|
97
97
|
def calculate_text_size_to_fit(self, draw, text, font_path, region):
|
|
98
98
|
font_size = 500 # Start with a large font size
|
|
99
|
-
font = ImageFont.truetype(font_path, size=font_size) if os.path.exists(font_path) else ImageFont.load_default()
|
|
99
|
+
font = ImageFont.truetype(font_path, size=font_size) if font_path and os.path.exists(font_path) else ImageFont.load_default()
|
|
100
100
|
|
|
101
101
|
def get_text_size(text, font):
|
|
102
102
|
bbox = draw.textbbox((0, 0), text, font=font)
|
|
@@ -117,7 +117,7 @@ class VideoGenerator:
|
|
|
117
117
|
|
|
118
118
|
# Reset font size for two-line layout
|
|
119
119
|
font_size = 500
|
|
120
|
-
font = ImageFont.truetype(font_path, size=font_size) if os.path.exists(font_path) else ImageFont.load_default()
|
|
120
|
+
font = ImageFont.truetype(font_path, size=font_size) if font_path and os.path.exists(font_path) else ImageFont.load_default()
|
|
121
121
|
|
|
122
122
|
while True:
|
|
123
123
|
text_width1, text_height1 = get_text_size(line1, font)
|
|
@@ -134,9 +134,9 @@ class VideoGenerator:
|
|
|
134
134
|
font_size -= 10
|
|
135
135
|
if font_size <= 0:
|
|
136
136
|
raise ValueError("Cannot fit text within the defined region.")
|
|
137
|
-
font = ImageFont.truetype(font_path, size=font_size) if os.path.exists(font_path) else ImageFont.load_default()
|
|
137
|
+
font = ImageFont.truetype(font_path, size=font_size) if font_path and os.path.exists(font_path) else ImageFont.load_default()
|
|
138
138
|
|
|
139
|
-
font = ImageFont.truetype(font_path, size=font_size) if os.path.exists(font_path) else ImageFont.load_default()
|
|
139
|
+
font = ImageFont.truetype(font_path, size=font_size) if font_path and os.path.exists(font_path) else ImageFont.load_default()
|
|
140
140
|
text_width, text_height = get_text_size(text, font)
|
|
141
141
|
|
|
142
142
|
return font, text
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: karaoke-gen
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.56.0
|
|
4
4
|
Summary: Generate karaoke videos with synchronized lyrics. Handles the entire process from downloading audio and lyrics to creating the final video with title screens.
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Andrew Beveridge
|
|
7
7
|
Author-email: andrew@beveridge.uk
|
|
8
|
-
Requires-Python: >=3.10,<3.
|
|
8
|
+
Requires-Python: >=3.10,<3.14
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
15
|
Requires-Dist: argparse (>=1.4.0)
|
|
15
16
|
Requires-Dist: attrs (>=24.2.0)
|
|
16
|
-
Requires-Dist: audio-separator[cpu] (>=0.
|
|
17
|
+
Requires-Dist: audio-separator[cpu] (>=0.34.0)
|
|
17
18
|
Requires-Dist: beautifulsoup4 (>=4)
|
|
18
19
|
Requires-Dist: cattrs (>=24.1.2)
|
|
19
20
|
Requires-Dist: fetch-lyrics-from-genius (>=0.1)
|
|
@@ -24,31 +25,38 @@ Requires-Dist: google-auth-httplib2
|
|
|
24
25
|
Requires-Dist: google-auth-oauthlib
|
|
25
26
|
Requires-Dist: kbputils (>=0.0.16,<0.0.17)
|
|
26
27
|
Requires-Dist: lyrics-converter (>=0.2.1)
|
|
27
|
-
Requires-Dist: lyrics-transcriber (>=0.
|
|
28
|
+
Requires-Dist: lyrics-transcriber (>=0.58)
|
|
28
29
|
Requires-Dist: lyricsgenius (>=3)
|
|
29
|
-
Requires-Dist:
|
|
30
|
+
Requires-Dist: modal (>=1.0.5,<2.0.0)
|
|
31
|
+
Requires-Dist: numpy (>=2)
|
|
30
32
|
Requires-Dist: pillow (>=10.1)
|
|
31
33
|
Requires-Dist: psutil (>=7.0.0,<8.0.0)
|
|
32
34
|
Requires-Dist: pyinstaller (>=6.3)
|
|
33
35
|
Requires-Dist: pyperclip
|
|
34
36
|
Requires-Dist: pytest-asyncio (>=0.23.5,<0.24.0)
|
|
37
|
+
Requires-Dist: python-multipart (>=0.0.20,<0.0.21)
|
|
35
38
|
Requires-Dist: requests (>=2)
|
|
36
39
|
Requires-Dist: thefuzz (>=0.22)
|
|
37
40
|
Requires-Dist: toml (>=0.10)
|
|
38
|
-
Requires-Dist: torch (
|
|
41
|
+
Requires-Dist: torch (>=2.7)
|
|
39
42
|
Requires-Dist: yt-dlp
|
|
40
|
-
Project-URL: Documentation, https://github.com/
|
|
41
|
-
Project-URL: Homepage, https://github.com/
|
|
42
|
-
Project-URL: Repository, https://github.com/
|
|
43
|
+
Project-URL: Documentation, https://github.com/nomadkaraoke/karaoke-gen/blob/main/README.md
|
|
44
|
+
Project-URL: Homepage, https://github.com/nomadkaraoke/karaoke-gen
|
|
45
|
+
Project-URL: Repository, https://github.com/nomadkaraoke/karaoke-gen
|
|
43
46
|
Description-Content-Type: text/markdown
|
|
44
47
|
|
|
45
|
-
# Karaoke
|
|
48
|
+
# Karaoke Generator 🎶 🎥 🚀
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+

|
|
51
|
+

|
|
52
|
+

|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
Generate karaoke videos with instrumental audio and synchronized lyrics. Handles the entire process from downloading audio and lyrics to creating the final video with title screens, uploading the resulting video to YouTube.
|
|
48
56
|
|
|
49
57
|
## Overview
|
|
50
58
|
|
|
51
|
-
Karaoke
|
|
59
|
+
Karaoke Generator is a comprehensive tool for creating high-quality karaoke videos. It automates the entire workflow:
|
|
52
60
|
|
|
53
61
|
1. **Download** audio and lyrics for a specified song
|
|
54
62
|
2. **Separate** audio stems (vocals, instrumental)
|
|
@@ -134,6 +142,25 @@ For a complete list of options:
|
|
|
134
142
|
karaoke-gen --help
|
|
135
143
|
```
|
|
136
144
|
|
|
145
|
+
## Development
|
|
146
|
+
|
|
147
|
+
### Running Tests
|
|
148
|
+
|
|
149
|
+
The project uses pytest for testing with unit and integration tests:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# Run all tests (unit tests first, then integration tests)
|
|
153
|
+
pytest
|
|
154
|
+
|
|
155
|
+
# Run only unit tests (fast feedback during development)
|
|
156
|
+
pytest -m "not integration"
|
|
157
|
+
|
|
158
|
+
# Run only integration tests (comprehensive end-to-end testing)
|
|
159
|
+
pytest -m integration
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Unit tests run quickly and provide fast feedback, while integration tests are slower but test the full workflow end-to-end.
|
|
163
|
+
|
|
137
164
|
## License
|
|
138
165
|
|
|
139
166
|
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
karaoke_gen/__init__.py,sha256=ViryQjs8ALc8A7mqJGHu028zajF5-Za_etFagXlo6kk,269
|
|
2
|
+
karaoke_gen/audio_processor.py,sha256=gqQo8dsG_4SEO5kwyT76DiU4jCNyiGpi6TT1R3imdGY,19591
|
|
3
|
+
karaoke_gen/config.py,sha256=I3h-940ZXvbrCNq_xcWHPMIB76cl-VNQYcK7-qgB-YI,6833
|
|
4
|
+
karaoke_gen/file_handler.py,sha256=c86-rGF7Fusl0uEIZFnreT7PJfK7lmUaEgauU8BBzjY,10024
|
|
5
|
+
karaoke_gen/karaoke_finalise/__init__.py,sha256=HqZ7TIhgt_tYZ-nb_NNCaejWAcF_aK-7wJY5TaW_keM,46
|
|
6
|
+
karaoke_gen/karaoke_finalise/karaoke_finalise.py,sha256=C2o9iRg5qgWc8qxNVzy8puA8W2ZtKJq28dnxXxS1RMs,56556
|
|
7
|
+
karaoke_gen/karaoke_gen.py,sha256=NEyb-AWLdqJL4nXg21o_YbutZVVbKt08TTPAYgSBDao,38052
|
|
8
|
+
karaoke_gen/lyrics_processor.py,sha256=054zBeYaCFti6lHRfYyHYdioM9YhBrpI52kHsSCh_KI,13363
|
|
9
|
+
karaoke_gen/metadata.py,sha256=TprFzWj-iJ7ghrXlHFMPzzqzuHzWeNvs3zGaND-z9Ds,6503
|
|
10
|
+
karaoke_gen/resources/AvenirNext-Bold.ttf,sha256=YxgKz2OP46lwLPCpIZhVa8COi_9KRDSXw4n8dIHHQSs,327048
|
|
11
|
+
karaoke_gen/resources/Montserrat-Bold.ttf,sha256=mLFIaBDC7M-qF9RhCoPBJ5TAeY716etBrqA4eUKSoYc,198120
|
|
12
|
+
karaoke_gen/resources/Oswald-Bold.ttf,sha256=S_2mLpNkBsDTe8FQRzrj1Qr-wloGETMJgoAcSKdi1lw,87604
|
|
13
|
+
karaoke_gen/resources/Oswald-SemiBold.ttf,sha256=G-vSJeeyEVft7D4s7FZQtGfXAViWPjzGCImV2a4u9d8,87608
|
|
14
|
+
karaoke_gen/resources/Zurich_Cn_BT_Bold.ttf,sha256=WNG5LOQ-uGUF_WWT5aQHzVbyWvQqGO5sZ4E-nRmvPuI,37780
|
|
15
|
+
karaoke_gen/utils/__init__.py,sha256=FpOHyeBRB06f3zMoLBUJHTDZACrabg-DoyBTxNKYyNY,722
|
|
16
|
+
karaoke_gen/utils/bulk_cli.py,sha256=uqAHnlidY-f_RhsQIHqZDnrznWRKhqpEDX2uiR1CUQs,18841
|
|
17
|
+
karaoke_gen/utils/gen_cli.py,sha256=sAZ-sau_3dI2hNBOZfiZqJjRf_cJFtuvZLy1V6URcxM,35688
|
|
18
|
+
karaoke_gen/video_generator.py,sha256=B7BQBrjkyvk3L3sctnPXnvr1rzkw0NYx5UCAl0ZiVx0,18464
|
|
19
|
+
karaoke_gen-0.56.0.dist-info/LICENSE,sha256=81R_4XwMZDODHD7JcZeUR8IiCU8AD7Ajl6bmwR9tYDk,1074
|
|
20
|
+
karaoke_gen-0.56.0.dist-info/METADATA,sha256=4zBpc8AJtrh7d6vuYczv5vdii4IdW7XjROzysGZji4E,5591
|
|
21
|
+
karaoke_gen-0.56.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
22
|
+
karaoke_gen-0.56.0.dist-info/entry_points.txt,sha256=IZY3O8i7m-qkmPuqgpAcxiS2fotNc6hC-CDWvNmoUEY,107
|
|
23
|
+
karaoke_gen-0.56.0.dist-info/RECORD,,
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
karaoke_prep/__init__.py,sha256=gwWt2-Z35n3jLfLG0phvz9IVZeM7rH5f3mflqUV79X4,38
|
|
2
|
-
karaoke_prep/audio_processor.py,sha256=Po2x3x5JyRe5yGUhJlzM5_mw9E7GijUbv-_hc_kAXqs,19131
|
|
3
|
-
karaoke_prep/config.py,sha256=I3h-940ZXvbrCNq_xcWHPMIB76cl-VNQYcK7-qgB-YI,6833
|
|
4
|
-
karaoke_prep/file_handler.py,sha256=sQNfEpqIir4Jd-V0VXUGZlsItt7pxZ8n_muzc1ONYck,8479
|
|
5
|
-
karaoke_prep/karaoke_finalise/__init__.py,sha256=HqZ7TIhgt_tYZ-nb_NNCaejWAcF_aK-7wJY5TaW_keM,46
|
|
6
|
-
karaoke_prep/karaoke_finalise/karaoke_finalise.py,sha256=sDW50vz4UMVEcrqB7WXx2bNT9Hvza-EOx8KqGHvGNYA,55917
|
|
7
|
-
karaoke_prep/karaoke_prep.py,sha256=nu5Ayg3UjlYiCICRr3hGQUNYRkFv6W3QvcCJEnQtFgM,37177
|
|
8
|
-
karaoke_prep/lyrics_processor.py,sha256=Yuax-FlcL_aLcLIPPAKyIjjwrNlnvAWGJOFlLbAUXEE,9994
|
|
9
|
-
karaoke_prep/metadata.py,sha256=PkwTnxX7fwbRmo_8ysC2zAMYSdZTtJtXWQypgNzssz8,4729
|
|
10
|
-
karaoke_prep/resources/AvenirNext-Bold.ttf,sha256=YxgKz2OP46lwLPCpIZhVa8COi_9KRDSXw4n8dIHHQSs,327048
|
|
11
|
-
karaoke_prep/resources/Montserrat-Bold.ttf,sha256=mLFIaBDC7M-qF9RhCoPBJ5TAeY716etBrqA4eUKSoYc,198120
|
|
12
|
-
karaoke_prep/resources/Oswald-Bold.ttf,sha256=S_2mLpNkBsDTe8FQRzrj1Qr-wloGETMJgoAcSKdi1lw,87604
|
|
13
|
-
karaoke_prep/resources/Oswald-SemiBold.ttf,sha256=G-vSJeeyEVft7D4s7FZQtGfXAViWPjzGCImV2a4u9d8,87608
|
|
14
|
-
karaoke_prep/resources/Zurich_Cn_BT_Bold.ttf,sha256=WNG5LOQ-uGUF_WWT5aQHzVbyWvQqGO5sZ4E-nRmvPuI,37780
|
|
15
|
-
karaoke_prep/utils/__init__.py,sha256=FpOHyeBRB06f3zMoLBUJHTDZACrabg-DoyBTxNKYyNY,722
|
|
16
|
-
karaoke_prep/utils/bulk_cli.py,sha256=Lezs8XNLk2Op0b6wmmupZGU3owgdMoLN4PFVRQQbRxM,18843
|
|
17
|
-
karaoke_prep/utils/gen_cli.py,sha256=vNhs0Oyi3sIkzNLvVXQCCsCF5Ku5bt75xdu4IsfAX3A,38326
|
|
18
|
-
karaoke_prep/video_generator.py,sha256=agtE7zDfY-4COjb7yT8aSPxvNGpOORV_lRtKczGbokM,18409
|
|
19
|
-
karaoke_gen-0.50.0.dist-info/LICENSE,sha256=81R_4XwMZDODHD7JcZeUR8IiCU8AD7Ajl6bmwR9tYDk,1074
|
|
20
|
-
karaoke_gen-0.50.0.dist-info/METADATA,sha256=WWboqA8QSLhczte82rbRqlvJxDG5SMeSND0lmT4dUso,4563
|
|
21
|
-
karaoke_gen-0.50.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
22
|
-
karaoke_gen-0.50.0.dist-info/entry_points.txt,sha256=8OkNvWtcr6Zceif8PbpYr6MBa2cFd_B1vJXP6Xcdyks,109
|
|
23
|
-
karaoke_gen-0.50.0.dist-info/RECORD,,
|
karaoke_prep/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .karaoke_prep import KaraokePrep
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|