karaoke-gen 0.50.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of karaoke-gen might be problematic. Click here for more details.

@@ -0,0 +1,873 @@
1
+ #!/usr/bin/env python
2
+ print("DEBUG: gen_cli.py starting imports...")
3
+ import argparse
4
+ import logging
5
+ from importlib import metadata
6
+ import tempfile
7
+ import os
8
+ import sys
9
+ import json
10
+ import asyncio
11
+ import time
12
+ 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.")
17
+
18
+
19
+ def is_url(string):
20
+ """Simple check to determine if a string is a URL."""
21
+ return string.startswith("http://") or string.startswith("https://")
22
+
23
+
24
+ def is_file(string):
25
+ """Check if a string is a valid file."""
26
+ return os.path.isfile(string)
27
+
28
+
29
+ async def async_main():
30
+ print("DEBUG: async_main() started.")
31
+ logger = logging.getLogger(__name__)
32
+ log_handler = logging.StreamHandler()
33
+ log_formatter = logging.Formatter(fmt="%(asctime)s.%(msecs)03d - %(levelname)s - %(module)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
34
+ log_handler.setFormatter(log_formatter)
35
+ logger.addHandler(log_handler)
36
+
37
+ print("DEBUG: async_main() logger configured.")
38
+
39
+ parser = argparse.ArgumentParser(
40
+ description="Generate karaoke videos with synchronized lyrics. Handles the entire process from downloading audio and lyrics to creating the final video.",
41
+ formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=54),
42
+ )
43
+
44
+ # Basic information
45
+ parser.add_argument(
46
+ "args",
47
+ nargs="*",
48
+ help="[Media or playlist URL] [Artist] [Title] of song to process. If URL is provided, Artist and Title are optional but increase chance of fetching the correct lyrics. If Artist and Title are provided with no URL, the top YouTube search result will be fetched.",
49
+ )
50
+
51
+ # Get version using importlib.metadata
52
+ try:
53
+ package_version = metadata.version("karaoke-gen")
54
+ except metadata.PackageNotFoundError:
55
+ package_version = "unknown"
56
+ print("DEBUG: Could not find version for karaoke-gen")
57
+
58
+ parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {package_version}")
59
+
60
+ # Workflow control
61
+ workflow_group = parser.add_argument_group("Workflow Control")
62
+ workflow_group.add_argument(
63
+ "--prep-only",
64
+ action="store_true",
65
+ help="Only run the preparation phase (download audio, lyrics, separate stems, create title screens). Example: --prep-only",
66
+ )
67
+ workflow_group.add_argument(
68
+ "--finalise-only",
69
+ action="store_true",
70
+ help="Only run the finalisation phase (remux, encode, organize). Must be run in a directory prepared by the prep phase. Example: --finalise-only",
71
+ )
72
+ workflow_group.add_argument(
73
+ "--skip-transcription",
74
+ action="store_true",
75
+ help="Skip automatic lyrics transcription/synchronization. Use this to fall back to manual syncing. Example: --skip-transcription",
76
+ )
77
+ workflow_group.add_argument(
78
+ "--skip-separation",
79
+ action="store_true",
80
+ help="Skip audio separation process. Example: --skip-separation",
81
+ )
82
+ workflow_group.add_argument(
83
+ "--skip-lyrics",
84
+ action="store_true",
85
+ help="Skip fetching and processing lyrics. Example: --skip-lyrics",
86
+ )
87
+ workflow_group.add_argument(
88
+ "--lyrics-only",
89
+ action="store_true",
90
+ help="Only process lyrics, skipping audio separation and title/end screen generation. Example: --lyrics-only",
91
+ )
92
+ workflow_group.add_argument(
93
+ "--edit-lyrics",
94
+ action="store_true",
95
+ help="Edit lyrics of an existing track. This will backup existing outputs, re-run the lyrics transcription process, and update all outputs. Example: --edit-lyrics",
96
+ )
97
+
98
+ # Logging & Debugging
99
+ debug_group = parser.add_argument_group("Logging & Debugging")
100
+ debug_group.add_argument(
101
+ "--log_level",
102
+ default="info",
103
+ help="Optional: logging level, e.g. info, debug, warning (default: %(default)s). Example: --log_level=debug",
104
+ )
105
+ debug_group.add_argument(
106
+ "--dry_run",
107
+ action="store_true",
108
+ help="Optional: perform a dry run without making any changes. Example: --dry_run",
109
+ )
110
+ debug_group.add_argument(
111
+ "--render_bounding_boxes",
112
+ action="store_true",
113
+ help="Optional: render bounding boxes around text regions for debugging. Example: --render_bounding_boxes",
114
+ )
115
+
116
+ # Input/Output Configuration
117
+ io_group = parser.add_argument_group("Input/Output Configuration")
118
+ io_group.add_argument(
119
+ "--filename_pattern",
120
+ help="Required if processing a folder: Python regex pattern to extract track names from filenames. Must contain a named group 'title'. Example: --filename_pattern='(?P<index>\\d+) - (?P<title>.+).mp3'",
121
+ )
122
+ io_group.add_argument(
123
+ "--output_dir",
124
+ default=".",
125
+ help="Optional: directory to write output files (default: <current dir>). Example: --output_dir=/app/karaoke",
126
+ )
127
+ io_group.add_argument(
128
+ "--no_track_subfolders",
129
+ action="store_false",
130
+ dest="no_track_subfolders",
131
+ help="Optional: do NOT create a named subfolder for each track. Example: --no_track_subfolders",
132
+ )
133
+ io_group.add_argument(
134
+ "--lossless_output_format",
135
+ default="FLAC",
136
+ help="Optional: lossless output format for separated audio (default: FLAC). Example: --lossless_output_format=WAV",
137
+ )
138
+ io_group.add_argument(
139
+ "--output_png",
140
+ type=lambda x: (str(x).lower() == "true"),
141
+ default=True,
142
+ help="Optional: output PNG format for title and end images (default: %(default)s). Example: --output_png=False",
143
+ )
144
+ io_group.add_argument(
145
+ "--output_jpg",
146
+ type=lambda x: (str(x).lower() == "true"),
147
+ default=True,
148
+ help="Optional: output JPG format for title and end images (default: %(default)s). Example: --output_jpg=False",
149
+ )
150
+
151
+ # Audio Processing Configuration
152
+ audio_group = parser.add_argument_group("Audio Processing Configuration")
153
+ audio_group.add_argument(
154
+ "--clean_instrumental_model",
155
+ default="model_bs_roformer_ep_317_sdr_12.9755.ckpt",
156
+ help="Optional: Model for clean instrumental separation (default: %(default)s).",
157
+ )
158
+ audio_group.add_argument(
159
+ "--backing_vocals_models",
160
+ nargs="+",
161
+ default=["mel_band_roformer_karaoke_aufr33_viperx_sdr_10.1956.ckpt"],
162
+ help="Optional: List of models for backing vocals separation (default: %(default)s).",
163
+ )
164
+ audio_group.add_argument(
165
+ "--other_stems_models",
166
+ nargs="+",
167
+ default=["htdemucs_6s.yaml"],
168
+ help="Optional: List of models for other stems separation (default: %(default)s).",
169
+ )
170
+
171
+ default_model_dir_unix = "/tmp/audio-separator-models/"
172
+ if os.name == "posix" and os.path.exists(default_model_dir_unix):
173
+ default_model_dir = default_model_dir_unix
174
+ else:
175
+ # Use tempfile to get the platform-independent temp directory
176
+ default_model_dir = os.path.join(tempfile.gettempdir(), "audio-separator-models")
177
+
178
+ audio_group.add_argument(
179
+ "--model_file_dir",
180
+ default=default_model_dir,
181
+ help="Optional: model files directory (default: %(default)s). Example: --model_file_dir=/app/models",
182
+ )
183
+ audio_group.add_argument(
184
+ "--existing_instrumental",
185
+ help="Optional: Path to an existing instrumental audio file. If provided, audio separation will be skipped.",
186
+ )
187
+ audio_group.add_argument(
188
+ "--instrumental_format",
189
+ default="flac",
190
+ help="Optional: format / file extension for instrumental track to use for remux (default: %(default)s). Example: --instrumental_format=mp3",
191
+ )
192
+
193
+ # Lyrics Configuration
194
+ lyrics_group = parser.add_argument_group("Lyrics Configuration")
195
+ lyrics_group.add_argument(
196
+ "--lyrics_artist",
197
+ help="Optional: Override the artist name used for lyrics search. Example: --lyrics_artist='The Beatles'",
198
+ )
199
+ lyrics_group.add_argument(
200
+ "--lyrics_title",
201
+ help="Optional: Override the song title used for lyrics search. Example: --lyrics_title='Hey Jude'",
202
+ )
203
+ lyrics_group.add_argument(
204
+ "--lyrics_file",
205
+ help="Optional: Path to a file containing lyrics to use instead of fetching from online. Example: --lyrics_file='/path/to/lyrics.txt'",
206
+ )
207
+ lyrics_group.add_argument(
208
+ "--subtitle_offset_ms",
209
+ type=int,
210
+ default=0,
211
+ help="Optional: Adjust subtitle timing by N milliseconds (+ve delays, -ve advances). Example: --subtitle_offset_ms=500",
212
+ )
213
+ lyrics_group.add_argument(
214
+ "--skip_transcription_review",
215
+ action="store_true",
216
+ help="Optional: Skip the review step after transcription. Example: --skip_transcription_review",
217
+ )
218
+
219
+ # Style Configuration
220
+ style_group = parser.add_argument_group("Style Configuration")
221
+ style_group.add_argument(
222
+ "--style_params_json",
223
+ help="Optional: Path to JSON file containing style configuration. Example: --style_params_json='/path/to/style_params.json'",
224
+ )
225
+
226
+ # Finalisation Configuration
227
+ finalise_group = parser.add_argument_group("Finalisation Configuration")
228
+ finalise_group.add_argument(
229
+ "--enable_cdg",
230
+ action="store_true",
231
+ help="Optional: Enable CDG ZIP generation during finalisation. Example: --enable_cdg",
232
+ )
233
+ finalise_group.add_argument(
234
+ "--enable_txt",
235
+ action="store_true",
236
+ help="Optional: Enable TXT ZIP generation during finalisation. Example: --enable_txt",
237
+ )
238
+ finalise_group.add_argument(
239
+ "--brand_prefix",
240
+ help="Optional: Your brand prefix to calculate the next sequential number. Example: --brand_prefix=BRAND",
241
+ )
242
+ finalise_group.add_argument(
243
+ "--organised_dir",
244
+ help="Optional: Target directory where the processed folder will be moved. Example: --organised_dir='/path/to/Tracks-Organized'",
245
+ )
246
+ finalise_group.add_argument(
247
+ "--organised_dir_rclone_root",
248
+ help="Optional: Rclone path which maps to your organised_dir. Example: --organised_dir_rclone_root='dropbox:Media/Karaoke/Tracks-Organized'",
249
+ )
250
+ finalise_group.add_argument(
251
+ "--public_share_dir",
252
+ help="Optional: Public share directory for final files. Example: --public_share_dir='/path/to/Tracks-PublicShare'",
253
+ )
254
+ finalise_group.add_argument(
255
+ "--youtube_client_secrets_file",
256
+ help="Optional: Path to youtube client secrets file. Example: --youtube_client_secrets_file='/path/to/client_secret.json'",
257
+ )
258
+ finalise_group.add_argument(
259
+ "--youtube_description_file",
260
+ help="Optional: Path to youtube description template. Example: --youtube_description_file='/path/to/description.txt'",
261
+ )
262
+ finalise_group.add_argument(
263
+ "--rclone_destination",
264
+ help="Optional: Rclone destination for public_share_dir sync. Example: --rclone_destination='googledrive:KaraokeFolder'",
265
+ )
266
+ finalise_group.add_argument(
267
+ "--discord_webhook_url",
268
+ help="Optional: Discord webhook URL for notifications. Example: --discord_webhook_url='https://discord.com/api/webhooks/...'",
269
+ )
270
+ finalise_group.add_argument(
271
+ "--email_template_file",
272
+ help="Optional: Path to email template file. Example: --email_template_file='/path/to/template.txt'",
273
+ )
274
+ finalise_group.add_argument(
275
+ "--keep-brand-code",
276
+ action="store_true",
277
+ help="Optional: Use existing brand code from current directory instead of generating new one. Example: --keep-brand-code",
278
+ )
279
+ finalise_group.add_argument(
280
+ "-y",
281
+ "--yes",
282
+ action="store_true",
283
+ help="Optional: Run in non-interactive mode, assuming yes to all prompts. Example: -y",
284
+ )
285
+ finalise_group.add_argument(
286
+ "--test_email_template",
287
+ action="store_true",
288
+ help="Optional: Test the email template functionality with fake data. Example: --test_email_template",
289
+ )
290
+
291
+ args = parser.parse_args()
292
+
293
+ print("DEBUG: async_main() args parsed.")
294
+
295
+ # Handle test email template case first
296
+ if args.test_email_template:
297
+ log_level = getattr(logging, args.log_level.upper())
298
+ logger.setLevel(log_level)
299
+ logger.info("Testing email template functionality...")
300
+ kfinalise = KaraokeFinalise(
301
+ log_formatter=log_formatter,
302
+ log_level=log_level,
303
+ email_template_file=args.email_template_file,
304
+ )
305
+ kfinalise.test_email_template()
306
+ return
307
+
308
+ print("DEBUG: async_main() continuing after test_email_template check.")
309
+
310
+ # Handle edit-lyrics mode
311
+ if args.edit_lyrics:
312
+ log_level = getattr(logging, args.log_level.upper())
313
+ logger.setLevel(log_level)
314
+ logger.info("Running in edit-lyrics mode...")
315
+
316
+ # Get the current directory name to extract artist and title
317
+ current_dir = os.path.basename(os.getcwd())
318
+ logger.info(f"Current directory: {current_dir}")
319
+
320
+ # Extract artist and title from directory name
321
+ # Format could be either "Artist - Title" or "BRAND-XXXX - Artist - Title"
322
+ if " - " not in current_dir:
323
+ logger.error("Current directory name does not contain ' - ' separator. Cannot extract artist and title.")
324
+ sys.exit(1)
325
+ return # Explicit return for testing
326
+
327
+ parts = current_dir.split(" - ")
328
+ if len(parts) == 2:
329
+ artist, title = parts
330
+ elif len(parts) >= 3:
331
+ # Handle brand code format: "BRAND-XXXX - Artist - Title"
332
+ artist = parts[1]
333
+ title = " - ".join(parts[2:])
334
+ else:
335
+ logger.error(f"Could not parse artist and title from directory name: {current_dir}")
336
+ sys.exit(1)
337
+ return # Explicit return for testing
338
+
339
+ logger.info(f"Extracted artist: {artist}, title: {title}")
340
+
341
+ # Initialize KaraokePrep
342
+ kprep_coroutine = KaraokePrep(
343
+ artist=artist,
344
+ title=title,
345
+ input_media=None, # Will be set by backup_existing_outputs
346
+ dry_run=args.dry_run,
347
+ log_formatter=log_formatter,
348
+ log_level=log_level,
349
+ render_bounding_boxes=args.render_bounding_boxes,
350
+ output_dir=".", # We're already in the track directory
351
+ create_track_subfolders=False, # Don't create subfolders, we're already in one
352
+ lossless_output_format=args.lossless_output_format,
353
+ output_png=args.output_png,
354
+ output_jpg=args.output_jpg,
355
+ clean_instrumental_model=args.clean_instrumental_model,
356
+ backing_vocals_models=args.backing_vocals_models,
357
+ other_stems_models=args.other_stems_models,
358
+ model_file_dir=args.model_file_dir,
359
+ skip_separation=True, # Skip separation as we already have the audio files
360
+ lyrics_artist=args.lyrics_artist or artist,
361
+ lyrics_title=args.lyrics_title or title,
362
+ lyrics_file=args.lyrics_file,
363
+ skip_lyrics=False, # We want to process lyrics
364
+ skip_transcription=False, # We want to transcribe
365
+ skip_transcription_review=args.skip_transcription_review,
366
+ subtitle_offset_ms=args.subtitle_offset_ms,
367
+ style_params_json=args.style_params_json,
368
+ )
369
+ # No await needed for constructor
370
+ kprep = kprep_coroutine
371
+
372
+ # Backup existing outputs and get the input audio file
373
+ track_output_dir = os.getcwd()
374
+ input_audio_wav = kprep.file_handler.backup_existing_outputs(track_output_dir, artist, title)
375
+ kprep.input_media = input_audio_wav
376
+
377
+ # Run KaraokePrep
378
+ tracks = await kprep.process()
379
+
380
+ # Load CDG styles if CDG generation is enabled
381
+ cdg_styles = None
382
+ if args.enable_cdg:
383
+ if not args.style_params_json:
384
+ logger.error("CDG styles JSON file path (--style_params_json) is required when --enable_cdg is used")
385
+ sys.exit(1)
386
+ return # Explicit return for testing
387
+ try:
388
+ with open(args.style_params_json, "r") as f:
389
+ style_params = json.loads(f.read())
390
+ cdg_styles = style_params["cdg"]
391
+ except FileNotFoundError:
392
+ logger.error(f"CDG styles configuration file not found: {args.style_params_json}")
393
+ sys.exit(1)
394
+ return # Explicit return for testing
395
+ except json.JSONDecodeError as e:
396
+ logger.error(f"Invalid JSON in CDG styles configuration file: {e}")
397
+ sys.exit(1)
398
+ return # Explicit return for testing
399
+ except KeyError:
400
+ logger.error(f"'cdg' key not found in style parameters file: {args.style_params_json}")
401
+ sys.exit(1)
402
+ return # Explicit return for testing
403
+
404
+ # Run KaraokeFinalise with keep_brand_code=True and replace_existing=True
405
+ kfinalise = KaraokeFinalise(
406
+ log_formatter=log_formatter,
407
+ log_level=log_level,
408
+ dry_run=args.dry_run,
409
+ instrumental_format=args.instrumental_format,
410
+ enable_cdg=args.enable_cdg,
411
+ enable_txt=args.enable_txt,
412
+ brand_prefix=args.brand_prefix,
413
+ organised_dir=args.organised_dir,
414
+ organised_dir_rclone_root=args.organised_dir_rclone_root,
415
+ public_share_dir=args.public_share_dir,
416
+ youtube_client_secrets_file=args.youtube_client_secrets_file,
417
+ youtube_description_file=args.youtube_description_file,
418
+ rclone_destination=args.rclone_destination,
419
+ discord_webhook_url=args.discord_webhook_url,
420
+ email_template_file=args.email_template_file,
421
+ cdg_styles=cdg_styles,
422
+ keep_brand_code=True, # Always keep brand code in edit mode
423
+ non_interactive=args.yes,
424
+ )
425
+
426
+ try:
427
+ final_track = kfinalise.process(replace_existing=True) # Replace existing YouTube video
428
+ logger.info(f"Successfully completed editing lyrics for: {artist} - {title}")
429
+
430
+ # Display summary of outputs
431
+ logger.info(f"Karaoke lyrics edit complete! Output files:")
432
+ logger.info(f"")
433
+ logger.info(f"Track: {final_track['artist']} - {final_track['title']}")
434
+ logger.info(f"")
435
+ logger.info(f"Working Files:")
436
+ logger.info(f" Video With Vocals: {final_track['video_with_vocals']}")
437
+ logger.info(f" Video With Instrumental: {final_track['video_with_instrumental']}")
438
+ logger.info(f"")
439
+ logger.info(f"Final Videos:")
440
+ logger.info(f" Lossless 4K MP4 (PCM): {final_track['final_video']}")
441
+ logger.info(f" Lossless 4K MKV (FLAC): {final_track['final_video_mkv']}")
442
+ logger.info(f" Lossy 4K MP4 (AAC): {final_track['final_video_lossy']}")
443
+ logger.info(f" Lossy 720p MP4 (AAC): {final_track['final_video_720p']}")
444
+
445
+ if "final_karaoke_cdg_zip" in final_track or "final_karaoke_txt_zip" in final_track:
446
+ logger.info(f"")
447
+ logger.info(f"Karaoke Files:")
448
+
449
+ if "final_karaoke_cdg_zip" in final_track:
450
+ logger.info(f" CDG+MP3 ZIP: {final_track['final_karaoke_cdg_zip']}")
451
+
452
+ if "final_karaoke_txt_zip" in final_track:
453
+ logger.info(f" TXT+MP3 ZIP: {final_track['final_karaoke_txt_zip']}")
454
+
455
+ if final_track["brand_code"]:
456
+ logger.info(f"")
457
+ logger.info(f"Organization:")
458
+ logger.info(f" Brand Code: {final_track['brand_code']}")
459
+ logger.info(f" Directory: {final_track['new_brand_code_dir_path']}")
460
+
461
+ if final_track["youtube_url"] or final_track["brand_code_dir_sharing_link"]:
462
+ logger.info(f"")
463
+ logger.info(f"Sharing:")
464
+
465
+ if final_track["brand_code_dir_sharing_link"]:
466
+ logger.info(f" Folder Link: {final_track['brand_code_dir_sharing_link']}")
467
+ try:
468
+ time.sleep(1) # Brief pause between clipboard operations
469
+ pyperclip.copy(final_track["brand_code_dir_sharing_link"])
470
+ logger.info(f" (Folder link copied to clipboard)")
471
+ except Exception as e:
472
+ logger.warning(f" Failed to copy folder link to clipboard: {str(e)}")
473
+
474
+ if final_track["youtube_url"]:
475
+ logger.info(f" YouTube URL: {final_track['youtube_url']}")
476
+ try:
477
+ pyperclip.copy(final_track["youtube_url"])
478
+ logger.info(f" (YouTube URL copied to clipboard)")
479
+ except Exception as e:
480
+ logger.warning(f" Failed to copy YouTube URL to clipboard: {str(e)}")
481
+
482
+ except Exception as e:
483
+ logger.error(f"Error during finalisation: {str(e)}")
484
+ raise e
485
+
486
+ return
487
+
488
+ print("DEBUG: async_main() continuing after edit_lyrics check.")
489
+
490
+ # Handle finalise-only mode
491
+ if args.finalise_only:
492
+ log_level = getattr(logging, args.log_level.upper())
493
+ logger.setLevel(log_level)
494
+ logger.info("Running in finalise-only mode...")
495
+
496
+ # Load CDG styles if CDG generation is enabled
497
+ cdg_styles = None
498
+ if args.enable_cdg:
499
+ if not args.style_params_json:
500
+ logger.error("CDG styles JSON file path (--style_params_json) is required when --enable_cdg is used")
501
+ sys.exit(1)
502
+ return # Explicit return for testing
503
+ try:
504
+ with open(args.style_params_json, "r") as f:
505
+ style_params = json.loads(f.read())
506
+ cdg_styles = style_params["cdg"]
507
+ except FileNotFoundError:
508
+ logger.error(f"CDG styles configuration file not found: {args.style_params_json}")
509
+ sys.exit(1)
510
+ return # Explicit return for testing
511
+ except json.JSONDecodeError as e:
512
+ logger.error(f"Invalid JSON in CDG styles configuration file: {e}")
513
+ sys.exit(1)
514
+ return # Explicit return for testing
515
+
516
+ kfinalise = KaraokeFinalise(
517
+ log_formatter=log_formatter,
518
+ log_level=log_level,
519
+ dry_run=args.dry_run,
520
+ instrumental_format=args.instrumental_format,
521
+ enable_cdg=args.enable_cdg,
522
+ enable_txt=args.enable_txt,
523
+ brand_prefix=args.brand_prefix,
524
+ organised_dir=args.organised_dir,
525
+ organised_dir_rclone_root=args.organised_dir_rclone_root,
526
+ public_share_dir=args.public_share_dir,
527
+ youtube_client_secrets_file=args.youtube_client_secrets_file,
528
+ youtube_description_file=args.youtube_description_file,
529
+ rclone_destination=args.rclone_destination,
530
+ discord_webhook_url=args.discord_webhook_url,
531
+ email_template_file=args.email_template_file,
532
+ cdg_styles=cdg_styles,
533
+ keep_brand_code=args.keep_brand_code,
534
+ non_interactive=args.yes,
535
+ )
536
+
537
+ try:
538
+ track = kfinalise.process()
539
+ logger.info(f"Karaoke finalisation processing complete! Output files:")
540
+ logger.info(f"")
541
+ logger.info(f"Track: {track['artist']} - {track['title']}")
542
+ logger.info(f"")
543
+ logger.info(f"Working Files:")
544
+ logger.info(f" Video With Vocals: {track['video_with_vocals']}")
545
+ logger.info(f" Video With Instrumental: {track['video_with_instrumental']}")
546
+ logger.info(f"")
547
+ logger.info(f"Final Videos:")
548
+ logger.info(f" Lossless 4K MP4 (PCM): {track['final_video']}")
549
+ logger.info(f" Lossless 4K MKV (FLAC): {track['final_video_mkv']}")
550
+ logger.info(f" Lossy 4K MP4 (AAC): {track['final_video_lossy']}")
551
+ logger.info(f" Lossy 720p MP4 (AAC): {track['final_video_720p']}")
552
+
553
+ if "final_karaoke_cdg_zip" in track or "final_karaoke_txt_zip" in track:
554
+ logger.info(f"")
555
+ logger.info(f"Karaoke Files:")
556
+
557
+ if "final_karaoke_cdg_zip" in track:
558
+ logger.info(f" CDG+MP3 ZIP: {track['final_karaoke_cdg_zip']}")
559
+
560
+ if "final_karaoke_txt_zip" in track:
561
+ logger.info(f" TXT+MP3 ZIP: {track['final_karaoke_txt_zip']}")
562
+
563
+ if track["brand_code"]:
564
+ logger.info(f"")
565
+ logger.info(f"Organization:")
566
+ logger.info(f" Brand Code: {track['brand_code']}")
567
+ logger.info(f" New Directory: {track['new_brand_code_dir_path']}")
568
+
569
+ if track["youtube_url"] or track["brand_code_dir_sharing_link"]:
570
+ logger.info(f"")
571
+ logger.info(f"Sharing:")
572
+
573
+ if track["brand_code_dir_sharing_link"]:
574
+ logger.info(f" Folder Link: {track['brand_code_dir_sharing_link']}")
575
+ try:
576
+ time.sleep(1) # Brief pause between clipboard operations
577
+ pyperclip.copy(track["brand_code_dir_sharing_link"])
578
+ logger.info(f" (Folder link copied to clipboard)")
579
+ except Exception as e:
580
+ logger.warning(f" Failed to copy folder link to clipboard: {str(e)}")
581
+
582
+ if track["youtube_url"]:
583
+ logger.info(f" YouTube URL: {track['youtube_url']}")
584
+ try:
585
+ pyperclip.copy(track["youtube_url"])
586
+ logger.info(f" (YouTube URL copied to clipboard)")
587
+ except Exception as e:
588
+ logger.warning(f" Failed to copy YouTube URL to clipboard: {str(e)}")
589
+ except Exception as e:
590
+ logger.error(f"An error occurred during finalisation, see stack trace below: {str(e)}")
591
+ raise e
592
+
593
+ return
594
+
595
+ print("DEBUG: async_main() parsed positional args.")
596
+
597
+ # For prep or full workflow, parse input arguments
598
+ input_media, artist, title, filename_pattern = None, None, None, None
599
+
600
+ if not args.args:
601
+ parser.print_help()
602
+ sys.exit(1)
603
+ return # Explicit return for testing
604
+
605
+ # Allow 3 forms of positional arguments:
606
+ # 1. URL or Media File only (may be single track URL, playlist URL, or local file)
607
+ # 2. Artist and Title only
608
+ # 3. URL, Artist, and Title
609
+ if args.args and (is_url(args.args[0]) or is_file(args.args[0])):
610
+ input_media = args.args[0]
611
+ if len(args.args) > 2:
612
+ artist = args.args[1]
613
+ title = args.args[2]
614
+ elif len(args.args) > 1:
615
+ artist = args.args[1]
616
+ else:
617
+ logger.warning("Input media provided without Artist and Title, both will be guessed from title")
618
+
619
+ elif os.path.isdir(args.args[0]):
620
+ if not args.filename_pattern:
621
+ logger.error("Filename pattern is required when processing a folder.")
622
+ sys.exit(1)
623
+ return # Explicit return for testing
624
+ if len(args.args) <= 1:
625
+ logger.error("Second parameter provided must be Artist name; Artist is required when processing a folder.")
626
+ sys.exit(1)
627
+ return # Explicit return for testing
628
+
629
+ input_media = args.args[0]
630
+ artist = args.args[1]
631
+ filename_pattern = args.filename_pattern
632
+
633
+ elif len(args.args) > 1:
634
+ artist = args.args[0]
635
+ title = args.args[1]
636
+ logger.warning(f"No input media provided, the top YouTube search result for {artist} - {title} will be used.")
637
+
638
+ else:
639
+ parser.print_help()
640
+ sys.exit(1)
641
+ return # Explicit return for testing
642
+
643
+ log_level = getattr(logging, args.log_level.upper())
644
+ logger.setLevel(log_level)
645
+
646
+ print("DEBUG: async_main() log level set.")
647
+
648
+ # Set up environment variables for lyrics-only mode
649
+ if args.lyrics_only:
650
+ args.skip_separation = True
651
+ os.environ["KARAOKE_PREP_SKIP_AUDIO_SEPARATION"] = "1"
652
+ os.environ["KARAOKE_PREP_SKIP_TITLE_END_SCREENS"] = "1"
653
+ logger.info("Lyrics-only mode enabled: skipping audio separation and title/end screen generation")
654
+
655
+ print("DEBUG: async_main() instantiating KaraokePrep...")
656
+
657
+ # Step 1: Run KaraokePrep
658
+ logger.info(f"KaraokePrep beginning with input_media: {input_media} artist: {artist} and title: {title}")
659
+ kprep_coroutine = KaraokePrep(
660
+ artist=artist,
661
+ title=title,
662
+ input_media=input_media,
663
+ filename_pattern=filename_pattern,
664
+ dry_run=args.dry_run,
665
+ log_formatter=log_formatter,
666
+ log_level=log_level,
667
+ render_bounding_boxes=args.render_bounding_boxes,
668
+ output_dir=args.output_dir,
669
+ create_track_subfolders=args.no_track_subfolders,
670
+ lossless_output_format=args.lossless_output_format,
671
+ output_png=args.output_png,
672
+ output_jpg=args.output_jpg,
673
+ clean_instrumental_model=args.clean_instrumental_model,
674
+ backing_vocals_models=args.backing_vocals_models,
675
+ other_stems_models=args.other_stems_models,
676
+ model_file_dir=args.model_file_dir,
677
+ existing_instrumental=args.existing_instrumental,
678
+ skip_separation=args.skip_separation,
679
+ lyrics_artist=args.lyrics_artist,
680
+ lyrics_title=args.lyrics_title,
681
+ lyrics_file=args.lyrics_file,
682
+ skip_lyrics=args.skip_lyrics,
683
+ skip_transcription=args.skip_transcription,
684
+ skip_transcription_review=args.skip_transcription_review,
685
+ subtitle_offset_ms=args.subtitle_offset_ms,
686
+ style_params_json=args.style_params_json,
687
+ )
688
+ # No await needed for constructor
689
+ kprep = kprep_coroutine
690
+
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.")
700
+
701
+ # If prep-only mode, display detailed output and exit
702
+ 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.")
742
+ return
743
+
744
+ print("DEBUG: async_main() continuing after prep_only check.")
745
+
746
+ # Step 2: For each track, run KaraokeFinalise
747
+ for track in tracks:
748
+ print(f"DEBUG: async_main() starting finalise loop for track: {track.get('track_output_dir')}")
749
+ logger.info(f"Starting finalisation phase for {track['artist']} - {track['title']}...")
750
+
751
+ # Use the track directory that was actually created by KaraokePrep
752
+ track_dir = track["track_output_dir"]
753
+ if not os.path.exists(track_dir):
754
+ logger.error(f"Track directory not found: {track_dir}")
755
+ continue
756
+
757
+ logger.info(f"Changing to directory: {track_dir}")
758
+ os.chdir(track_dir)
759
+
760
+ # Load CDG styles if CDG generation is enabled
761
+ cdg_styles = None
762
+ if args.enable_cdg:
763
+ if not args.style_params_json:
764
+ logger.error("CDG styles JSON file path (--style_params_json) is required when --enable_cdg is used")
765
+ sys.exit(1)
766
+ return # Explicit return for testing
767
+ try:
768
+ with open(args.style_params_json, "r") as f:
769
+ style_params = json.loads(f.read())
770
+ cdg_styles = style_params["cdg"]
771
+ except FileNotFoundError:
772
+ logger.error(f"CDG styles configuration file not found: {args.style_params_json}")
773
+ sys.exit(1)
774
+ return # Explicit return for testing
775
+ except json.JSONDecodeError as e:
776
+ logger.error(f"Invalid JSON in CDG styles configuration file: {e}")
777
+ sys.exit(1)
778
+ return # Explicit return for testing
779
+
780
+ # Initialize KaraokeFinalise
781
+ kfinalise = KaraokeFinalise(
782
+ log_formatter=log_formatter,
783
+ log_level=log_level,
784
+ dry_run=args.dry_run,
785
+ instrumental_format=args.instrumental_format,
786
+ enable_cdg=args.enable_cdg,
787
+ enable_txt=args.enable_txt,
788
+ brand_prefix=args.brand_prefix,
789
+ organised_dir=args.organised_dir,
790
+ organised_dir_rclone_root=args.organised_dir_rclone_root,
791
+ public_share_dir=args.public_share_dir,
792
+ youtube_client_secrets_file=args.youtube_client_secrets_file,
793
+ youtube_description_file=args.youtube_description_file,
794
+ rclone_destination=args.rclone_destination,
795
+ discord_webhook_url=args.discord_webhook_url,
796
+ email_template_file=args.email_template_file,
797
+ cdg_styles=cdg_styles,
798
+ keep_brand_code=args.keep_brand_code,
799
+ non_interactive=args.yes,
800
+ )
801
+
802
+ try:
803
+ final_track = kfinalise.process()
804
+ logger.info(f"Successfully completed processing for: {track['artist']} - {track['title']}")
805
+
806
+ # Display summary of outputs
807
+ logger.info(f"Karaoke processing complete! Output files:")
808
+ logger.info(f"")
809
+ logger.info(f"Track: {final_track['artist']} - {final_track['title']}")
810
+ logger.info(f"")
811
+ logger.info(f"Working Files:")
812
+ logger.info(f" Video With Vocals: {final_track['video_with_vocals']}")
813
+ logger.info(f" Video With Instrumental: {final_track['video_with_instrumental']}")
814
+ logger.info(f"")
815
+ logger.info(f"Final Videos:")
816
+ logger.info(f" Lossless 4K MP4 (PCM): {final_track['final_video']}")
817
+ logger.info(f" Lossless 4K MKV (FLAC): {final_track['final_video_mkv']}")
818
+ logger.info(f" Lossy 4K MP4 (AAC): {final_track['final_video_lossy']}")
819
+ logger.info(f" Lossy 720p MP4 (AAC): {final_track['final_video_720p']}")
820
+
821
+ if "final_karaoke_cdg_zip" in final_track or "final_karaoke_txt_zip" in final_track:
822
+ logger.info(f"")
823
+ logger.info(f"Karaoke Files:")
824
+
825
+ if "final_karaoke_cdg_zip" in final_track:
826
+ logger.info(f" CDG+MP3 ZIP: {final_track['final_karaoke_cdg_zip']}")
827
+
828
+ if "final_karaoke_txt_zip" in final_track:
829
+ logger.info(f" TXT+MP3 ZIP: {final_track['final_karaoke_txt_zip']}")
830
+
831
+ if final_track["brand_code"]:
832
+ logger.info(f"")
833
+ logger.info(f"Organization:")
834
+ logger.info(f" Brand Code: {final_track['brand_code']}")
835
+ logger.info(f" New Directory: {final_track['new_brand_code_dir_path']}")
836
+
837
+ if final_track["youtube_url"] or final_track["brand_code_dir_sharing_link"]:
838
+ logger.info(f"")
839
+ logger.info(f"Sharing:")
840
+
841
+ if final_track["brand_code_dir_sharing_link"]:
842
+ logger.info(f" Folder Link: {final_track['brand_code_dir_sharing_link']}")
843
+ try:
844
+ time.sleep(1) # Brief pause between clipboard operations
845
+ pyperclip.copy(final_track["brand_code_dir_sharing_link"])
846
+ logger.info(f" (Folder link copied to clipboard)")
847
+ except Exception as e:
848
+ logger.warning(f" Failed to copy folder link to clipboard: {str(e)}")
849
+
850
+ if final_track["youtube_url"]:
851
+ logger.info(f" YouTube URL: {final_track['youtube_url']}")
852
+ try:
853
+ pyperclip.copy(final_track["youtube_url"])
854
+ logger.info(f" (YouTube URL copied to clipboard)")
855
+ except Exception as e:
856
+ logger.warning(f" Failed to copy YouTube URL to clipboard: {str(e)}")
857
+
858
+ except Exception as e:
859
+ logger.error(f"Error during finalisation: {str(e)}")
860
+ raise e
861
+
862
+ print("DEBUG: async_main() finished.")
863
+
864
+
865
+ def main():
866
+ print("DEBUG: main() started.")
867
+ asyncio.run(async_main())
868
+ print("DEBUG: main() finished.")
869
+
870
+
871
+ if __name__ == "__main__":
872
+ print("DEBUG: __main__ block executing.")
873
+ main()