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.

Files changed (26) hide show
  1. karaoke_gen/__init__.py +7 -0
  2. {karaoke_prep → karaoke_gen}/audio_processor.py +21 -16
  3. {karaoke_prep → karaoke_gen}/file_handler.py +50 -14
  4. {karaoke_prep → karaoke_gen}/karaoke_finalise/karaoke_finalise.py +15 -2
  5. karaoke_prep/karaoke_prep.py → karaoke_gen/karaoke_gen.py +23 -10
  6. {karaoke_prep → karaoke_gen}/lyrics_processor.py +72 -12
  7. {karaoke_prep → karaoke_gen}/metadata.py +71 -21
  8. {karaoke_prep → karaoke_gen}/utils/bulk_cli.py +2 -2
  9. {karaoke_prep → karaoke_gen}/utils/gen_cli.py +32 -94
  10. {karaoke_prep → karaoke_gen}/video_generator.py +5 -5
  11. {karaoke_gen-0.50.0.dist-info → karaoke_gen-0.56.0.dist-info}/METADATA +39 -12
  12. karaoke_gen-0.56.0.dist-info/RECORD +23 -0
  13. karaoke_gen-0.56.0.dist-info/entry_points.txt +4 -0
  14. karaoke_gen-0.50.0.dist-info/RECORD +0 -23
  15. karaoke_gen-0.50.0.dist-info/entry_points.txt +0 -4
  16. karaoke_prep/__init__.py +0 -1
  17. {karaoke_prep → karaoke_gen}/config.py +0 -0
  18. {karaoke_prep → karaoke_gen}/karaoke_finalise/__init__.py +0 -0
  19. {karaoke_prep → karaoke_gen}/resources/AvenirNext-Bold.ttf +0 -0
  20. {karaoke_prep → karaoke_gen}/resources/Montserrat-Bold.ttf +0 -0
  21. {karaoke_prep → karaoke_gen}/resources/Oswald-Bold.ttf +0 -0
  22. {karaoke_prep → karaoke_gen}/resources/Oswald-SemiBold.ttf +0 -0
  23. {karaoke_prep → karaoke_gen}/resources/Zurich_Cn_BT_Bold.ttf +0 -0
  24. {karaoke_prep → karaoke_gen}/utils/__init__.py +0 -0
  25. {karaoke_gen-0.50.0.dist-info → karaoke_gen-0.56.0.dist-info}/LICENSE +0 -0
  26. {karaoke_gen-0.50.0.dist-info → karaoke_gen-0.56.0.dist-info}/WHEEL +0 -0
@@ -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"Renaming Vocals file {file} to {vocals_path}")
76
- os.rename(file, vocals_path)
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"Renaming Instrumental file {file} to {instrumental_path}")
79
- os.rename(file, instrumental_path)
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"Renaming other stem file {file} to {other_stem_path}")
90
- os.rename(file, other_stem_path)
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"Renaming other stem file {file} to {other_stem_path}")
102
- os.rename(file, other_stem_path)
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
- proc = psutil.Process(pid)
137
- cmd = " ".join(proc.cmdline())
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("KARAOKE_PREP_SKIP_AUDIO_SEPARATION"):
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
- os.rename(file, vocals_path)
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
- os.rename(file, instrumental_path)
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
- os.rename(file, other_stem_path)
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
- os.rename(file, lead_vocals_path)
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
- os.rename(file, backing_vocals_path)
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
- "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",
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
- 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
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
- shutil.copy2(output_files["final_karaoke_cdg_zip"], dest_zip_file)
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, processed_track["input_audio_wav"], lyrics_artist, lyrics_title, track_output_dir
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 future if separation won't run
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("KARAOKE_PREP_SKIP_TITLE_END_SCREENS"):
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("KARAOKE_PREP_SKIP_TITLE_END_SCREENS"):
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 {artist} - {title} from audio file: {input_audio_wav} with output directory: {track_output_dir}"
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(artist)
112
- sanitized_title = sanitize_filename(title)
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 subdirectory for new transcription
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=self.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=True,
221
+ generate_cdg=False, # Also defer CDG generation to Phase 2
175
222
  video_resolution="4k",
176
- enable_review=not self.skip_transcription_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=artist,
187
- title=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
- extracted_info = None
8
- if input_url is not None:
9
- # If a URL is provided, use it to extract the metadata
10
- with ydl({"quiet": True}) as ydl_instance:
11
- extracted_info = ydl_instance.extract_info(input_url, download=False)
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
- # If no URL is provided, use the query to search for the top result
14
- ydl_opts = {"quiet": "True", "format": "bestaudio", "noplaylist": "True", "extract_flat": True}
15
- with ydl(ydl_opts) as ydl_instance:
16
- query = f"{input_artist} {input_title}"
17
- search_results = ydl_instance.extract_info(f"ytsearch1:{query}", download=False)
18
- if search_results and "entries" in search_results and search_results["entries"]:
19
- extracted_info = search_results["entries"][0]
20
- else:
21
- # Raise IndexError to match the expected exception in tests
22
- raise IndexError(f"No search results found on YouTube for query: {input_artist} {input_title}")
23
-
24
- if not extracted_info:
25
- raise Exception(f"Failed to extract info for query: {input_artist} {input_title} or URL: {input_url}")
26
-
27
- return extracted_info
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 karaoke_prep import KaraokePrep
11
- from karaoke_prep.karaoke_finalise import KaraokeFinalise
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 karaoke_prep import KaraokePrep
14
- from karaoke_prep.karaoke_finalise import KaraokeFinalise
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.keep_brand_code,
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"Karaoke finalisation processing complete! Output files:")
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" New Directory: {track['new_brand_code_dir_path']}")
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["KARAOKE_PREP_SKIP_AUDIO_SEPARATION"] = "1"
652
- os.environ["KARAOKE_PREP_SKIP_TITLE_END_SCREENS"] = "1"
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
- print("DEBUG: async_main() KaraokePrep instantiated.")
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, display detailed output and exit
681
+ # If prep-only mode, we're done
702
682
  if args.prep_only:
703
- logger.info(f"Karaoke Prep complete! Output files:")
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.keep_brand_code,
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 for: {track['artist']} - {track['title']}")
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 processing complete! Output files:")
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" New Directory: {final_track['new_brand_code_dir_path']}")
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"Error during finalisation: {str(e)}")
800
+ logger.error(f"An error occurred during finalisation, see stack trace below: {str(e)}")
860
801
  raise e
861
-
862
- print("DEBUG: async_main() finished.")
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("karaoke_prep.resources", format["font"]) as font_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.50.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.13
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.21.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.34)
28
+ Requires-Dist: lyrics-transcriber (>=0.58)
28
29
  Requires-Dist: lyricsgenius (>=3)
29
- Requires-Dist: numpy (>=1,<2)
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 (<2.5)
41
+ Requires-Dist: torch (>=2.7)
39
42
  Requires-Dist: yt-dlp
40
- Project-URL: Documentation, https://github.com/karaokenerds/karaoke-gen/blob/main/README.md
41
- Project-URL: Homepage, https://github.com/karaokenerds/karaoke-gen
42
- Project-URL: Repository, https://github.com/karaokenerds/karaoke-gen
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 Gen
48
+ # Karaoke Generator 🎶 🎥 🚀
46
49
 
47
- Generate karaoke videos with synchronized lyrics. Handles the entire process from downloading audio and lyrics to creating the final video with title screens.
50
+ ![PyPI - Version](https://img.shields.io/pypi/v/karaoke-gen)
51
+ ![Python Version](https://img.shields.io/badge/python-3.10+-blue)
52
+ ![Tests](https://github.com/nomadkaraoke/karaoke-gen/workflows/Test%20and%20Publish/badge.svg)
53
+ ![Test Coverage](https://codecov.io/gh/nomadkaraoke/karaoke-gen/branch/main/graph/badge.svg)
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 Gen is a comprehensive tool for creating high-quality karaoke videos. It automates the entire workflow:
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,,
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ karaoke-bulk=karaoke_gen.utils.bulk_cli:main
3
+ karaoke-gen=karaoke_gen.utils.gen_cli:main
4
+
@@ -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,,
@@ -1,4 +0,0 @@
1
- [console_scripts]
2
- karaoke-bulk=karaoke_prep.utils.bulk_cli:main
3
- karaoke-gen=karaoke_prep.utils.gen_cli:main
4
-
karaoke_prep/__init__.py DELETED
@@ -1 +0,0 @@
1
- from .karaoke_prep import KaraokePrep
File without changes
File without changes
File without changes