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