karaoke-gen 0.50.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of karaoke-gen might be problematic. Click here for more details.
- karaoke_gen-0.50.0.dist-info/LICENSE +21 -0
- karaoke_gen-0.50.0.dist-info/METADATA +140 -0
- karaoke_gen-0.50.0.dist-info/RECORD +23 -0
- karaoke_gen-0.50.0.dist-info/WHEEL +4 -0
- karaoke_gen-0.50.0.dist-info/entry_points.txt +4 -0
- karaoke_prep/__init__.py +1 -0
- karaoke_prep/audio_processor.py +396 -0
- karaoke_prep/config.py +134 -0
- karaoke_prep/file_handler.py +186 -0
- karaoke_prep/karaoke_finalise/__init__.py +1 -0
- karaoke_prep/karaoke_finalise/karaoke_finalise.py +1163 -0
- karaoke_prep/karaoke_prep.py +687 -0
- karaoke_prep/lyrics_processor.py +225 -0
- karaoke_prep/metadata.py +105 -0
- karaoke_prep/resources/AvenirNext-Bold.ttf +0 -0
- karaoke_prep/resources/Montserrat-Bold.ttf +0 -0
- karaoke_prep/resources/Oswald-Bold.ttf +0 -0
- karaoke_prep/resources/Oswald-SemiBold.ttf +0 -0
- karaoke_prep/resources/Zurich_Cn_BT_Bold.ttf +0 -0
- karaoke_prep/utils/__init__.py +18 -0
- karaoke_prep/utils/bulk_cli.py +483 -0
- karaoke_prep/utils/gen_cli.py +873 -0
- karaoke_prep/video_generator.py +424 -0
|
@@ -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()
|