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,225 @@
1
+ import os
2
+ import re
3
+ import logging
4
+ import shutil
5
+ from lyrics_transcriber import LyricsTranscriber, OutputConfig, TranscriberConfig, LyricsConfig
6
+ from lyrics_transcriber.core.controller import LyricsControllerResult
7
+ from dotenv import load_dotenv
8
+ from .utils import sanitize_filename
9
+
10
+
11
+ # Placeholder class or functions for lyrics processing
12
+ class LyricsProcessor:
13
+ def __init__(
14
+ self, logger, style_params_json, lyrics_file, skip_transcription, skip_transcription_review, render_video, subtitle_offset_ms
15
+ ):
16
+ self.logger = logger
17
+ self.style_params_json = style_params_json
18
+ self.lyrics_file = lyrics_file
19
+ self.skip_transcription = skip_transcription
20
+ self.skip_transcription_review = skip_transcription_review
21
+ self.render_video = render_video
22
+ self.subtitle_offset_ms = subtitle_offset_ms
23
+
24
+ def find_best_split_point(self, line):
25
+ """
26
+ Find the best split point in a line based on the specified criteria.
27
+ """
28
+
29
+ self.logger.debug(f"Finding best_split_point for line: {line}")
30
+ words = line.split()
31
+ mid_word_index = len(words) // 2
32
+ self.logger.debug(f"words: {words} mid_word_index: {mid_word_index}")
33
+
34
+ # Check for a comma within one or two words of the middle word
35
+ if "," in line:
36
+ mid_point = len(" ".join(words[:mid_word_index]))
37
+ comma_indices = [i for i, char in enumerate(line) if char == ","]
38
+
39
+ for index in comma_indices:
40
+ if abs(mid_point - index) < 20 and len(line[: index + 1].strip()) <= 36:
41
+ self.logger.debug(
42
+ f"Found comma at index {index} which is within 20 characters of mid_point {mid_point} and results in a suitable line length, accepting as split point"
43
+ )
44
+ return index + 1 # Include the comma in the first line
45
+
46
+ # Check for 'and'
47
+ if " and " in line:
48
+ mid_point = len(line) // 2
49
+ and_indices = [m.start() for m in re.finditer(" and ", line)]
50
+ for index in sorted(and_indices, key=lambda x: abs(x - mid_point)):
51
+ if len(line[: index + len(" and ")].strip()) <= 36:
52
+ self.logger.debug(f"Found 'and' at index {index} which results in a suitable line length, accepting as split point")
53
+ return index + len(" and ")
54
+
55
+ # If no better split point is found, try splitting at the middle word
56
+ if len(words) > 2 and mid_word_index > 0:
57
+ split_at_middle = len(" ".join(words[:mid_word_index]))
58
+ if split_at_middle <= 36:
59
+ self.logger.debug(f"Splitting at middle word index: {mid_word_index}")
60
+ return split_at_middle
61
+
62
+ # If the line is still too long, forcibly split at the maximum length
63
+ forced_split_point = 36
64
+ if len(line) > forced_split_point:
65
+ self.logger.debug(f"Line is still too long, forcibly splitting at position {forced_split_point}")
66
+ return forced_split_point
67
+
68
+ def process_line(self, line):
69
+ """
70
+ Process a single line to ensure it's within the maximum length,
71
+ and handle parentheses.
72
+ """
73
+ processed_lines = []
74
+ iteration_count = 0
75
+ max_iterations = 100 # Failsafe limit
76
+
77
+ while len(line) > 36:
78
+ if iteration_count > max_iterations:
79
+ self.logger.error(f"Maximum iterations exceeded in process_line for line: {line}")
80
+ break
81
+
82
+ # Check if the line contains parentheses
83
+ if "(" in line and ")" in line:
84
+ start_paren = line.find("(")
85
+ end_paren = line.find(")") + 1
86
+ if end_paren < len(line) and line[end_paren] == ",":
87
+ end_paren += 1
88
+
89
+ if start_paren > 0:
90
+ processed_lines.append(line[:start_paren].strip())
91
+ processed_lines.append(line[start_paren:end_paren].strip())
92
+ line = line[end_paren:].strip()
93
+ else:
94
+ split_point = self.find_best_split_point(line)
95
+ processed_lines.append(line[:split_point].strip())
96
+ line = line[split_point:].strip()
97
+
98
+ iteration_count += 1
99
+
100
+ if line: # Add the remaining part if not empty
101
+ processed_lines.append(line)
102
+
103
+ return processed_lines
104
+
105
+ def transcribe_lyrics(self, input_audio_wav, artist, title, track_output_dir):
106
+ self.logger.info(
107
+ f"Transcribing lyrics for track {artist} - {title} from audio file: {input_audio_wav} with output directory: {track_output_dir}"
108
+ )
109
+
110
+ # Check for existing files first using sanitized names
111
+ sanitized_artist = sanitize_filename(artist)
112
+ sanitized_title = sanitize_filename(title)
113
+ parent_video_path = os.path.join(track_output_dir, f"{sanitized_artist} - {sanitized_title} (With Vocals).mkv")
114
+ parent_lrc_path = os.path.join(track_output_dir, f"{sanitized_artist} - {sanitized_title} (Karaoke).lrc")
115
+
116
+ # Check lyrics directory for existing files
117
+ lyrics_dir = os.path.join(track_output_dir, "lyrics")
118
+ lyrics_video_path = os.path.join(lyrics_dir, f"{sanitized_artist} - {sanitized_title} (With Vocals).mkv")
119
+ lyrics_lrc_path = os.path.join(lyrics_dir, f"{sanitized_artist} - {sanitized_title} (Karaoke).lrc")
120
+
121
+ # If files exist in parent directory, return early
122
+ if os.path.exists(parent_video_path) and os.path.exists(parent_lrc_path):
123
+ self.logger.info(f"Found existing video and LRC files in parent directory, skipping transcription")
124
+ return {
125
+ "lrc_filepath": parent_lrc_path,
126
+ "ass_filepath": parent_video_path,
127
+ }
128
+
129
+ # If files exist in lyrics directory, copy to parent and return
130
+ if os.path.exists(lyrics_video_path) and os.path.exists(lyrics_lrc_path):
131
+ self.logger.info(f"Found existing video and LRC files in lyrics directory, copying to parent")
132
+ os.makedirs(track_output_dir, exist_ok=True)
133
+ shutil.copy2(lyrics_video_path, parent_video_path)
134
+ shutil.copy2(lyrics_lrc_path, parent_lrc_path)
135
+ return {
136
+ "lrc_filepath": parent_lrc_path,
137
+ "ass_filepath": parent_video_path,
138
+ }
139
+
140
+ # Create lyrics subdirectory for new transcription
141
+ os.makedirs(lyrics_dir, exist_ok=True)
142
+ self.logger.info(f"Created lyrics directory: {lyrics_dir}")
143
+
144
+ # Load environment variables
145
+ load_dotenv()
146
+ env_config = {
147
+ "audioshake_api_token": os.getenv("AUDIOSHAKE_API_TOKEN"),
148
+ "genius_api_token": os.getenv("GENIUS_API_TOKEN"),
149
+ "spotify_cookie": os.getenv("SPOTIFY_COOKIE_SP_DC"),
150
+ "runpod_api_key": os.getenv("RUNPOD_API_KEY"),
151
+ "whisper_runpod_id": os.getenv("WHISPER_RUNPOD_ID"),
152
+ }
153
+
154
+ # Create config objects for LyricsTranscriber
155
+ transcriber_config = TranscriberConfig(
156
+ audioshake_api_token=env_config.get("audioshake_api_token"),
157
+ )
158
+
159
+ lyrics_config = LyricsConfig(
160
+ genius_api_token=env_config.get("genius_api_token"),
161
+ spotify_cookie=env_config.get("spotify_cookie"),
162
+ lyrics_file=self.lyrics_file,
163
+ )
164
+
165
+ output_config = OutputConfig(
166
+ output_styles_json=self.style_params_json,
167
+ output_dir=lyrics_dir,
168
+ render_video=self.render_video,
169
+ fetch_lyrics=True,
170
+ run_transcription=not self.skip_transcription,
171
+ run_correction=True,
172
+ generate_plain_text=True,
173
+ generate_lrc=True,
174
+ generate_cdg=True,
175
+ video_resolution="4k",
176
+ enable_review=not self.skip_transcription_review,
177
+ subtitle_offset_ms=self.subtitle_offset_ms,
178
+ )
179
+
180
+ # Add this log entry to debug the OutputConfig
181
+ self.logger.info(f"Instantiating LyricsTranscriber with OutputConfig: {output_config}")
182
+
183
+ # Initialize transcriber with new config objects
184
+ transcriber = LyricsTranscriber(
185
+ audio_filepath=input_audio_wav,
186
+ artist=artist,
187
+ title=title,
188
+ transcriber_config=transcriber_config,
189
+ lyrics_config=lyrics_config,
190
+ output_config=output_config,
191
+ logger=self.logger,
192
+ )
193
+
194
+ # Process and get results
195
+ results: LyricsControllerResult = transcriber.process()
196
+ self.logger.info(f"Transcriber Results Filepaths:")
197
+ for key, value in results.__dict__.items():
198
+ if key.endswith("_filepath"):
199
+ self.logger.info(f" {key}: {value}")
200
+
201
+ # Build output dictionary
202
+ transcriber_outputs = {}
203
+ if results.lrc_filepath:
204
+ transcriber_outputs["lrc_filepath"] = results.lrc_filepath
205
+ self.logger.info(f"Moving LRC file from {results.lrc_filepath} to {parent_lrc_path}")
206
+ shutil.copy2(results.lrc_filepath, parent_lrc_path)
207
+
208
+ if results.ass_filepath:
209
+ transcriber_outputs["ass_filepath"] = results.ass_filepath
210
+ self.logger.info(f"Moving video file from {results.video_filepath} to {parent_video_path}")
211
+ shutil.copy2(results.video_filepath, parent_video_path)
212
+
213
+ if results.transcription_corrected:
214
+ transcriber_outputs["corrected_lyrics_text"] = "\n".join(
215
+ segment.text for segment in results.transcription_corrected.corrected_segments
216
+ )
217
+ transcriber_outputs["corrected_lyrics_text_filepath"] = results.corrected_txt
218
+
219
+ if transcriber_outputs:
220
+ self.logger.info(f"*** Transcriber Filepath Outputs: ***")
221
+ for key, value in transcriber_outputs.items():
222
+ if key.endswith("_filepath"):
223
+ self.logger.info(f" {key}: {value}")
224
+
225
+ return transcriber_outputs
@@ -0,0 +1,105 @@
1
+ import logging
2
+ import yt_dlp.YoutubeDL as ydl
3
+
4
+ def extract_info_for_online_media(input_url, input_artist, input_title, logger):
5
+ """Extracts metadata using yt-dlp, either from a URL or via search."""
6
+ logger.info(f"Extracting info for input_url: {input_url} input_artist: {input_artist} input_title: {input_title}")
7
+ extracted_info = None
8
+ if input_url is not None:
9
+ # If a URL is provided, use it to extract the metadata
10
+ with ydl({"quiet": True}) as ydl_instance:
11
+ extracted_info = ydl_instance.extract_info(input_url, download=False)
12
+ else:
13
+ # If no URL is provided, use the query to search for the top result
14
+ ydl_opts = {"quiet": "True", "format": "bestaudio", "noplaylist": "True", "extract_flat": True}
15
+ with ydl(ydl_opts) as ydl_instance:
16
+ query = f"{input_artist} {input_title}"
17
+ search_results = ydl_instance.extract_info(f"ytsearch1:{query}", download=False)
18
+ if search_results and "entries" in search_results and search_results["entries"]:
19
+ extracted_info = search_results["entries"][0]
20
+ else:
21
+ # Raise IndexError to match the expected exception in tests
22
+ raise IndexError(f"No search results found on YouTube for query: {input_artist} {input_title}")
23
+
24
+ if not extracted_info:
25
+ raise Exception(f"Failed to extract info for query: {input_artist} {input_title} or URL: {input_url}")
26
+
27
+ return extracted_info
28
+
29
+
30
+ def parse_track_metadata(extracted_info, current_artist, current_title, persistent_artist, logger):
31
+ """
32
+ Parses extracted_info to determine URL, extractor, ID, artist, and title.
33
+ Returns a dictionary with the parsed values.
34
+ """
35
+ parsed_data = {
36
+ "url": None,
37
+ "extractor": None,
38
+ "media_id": None,
39
+ "artist": current_artist,
40
+ "title": current_title,
41
+ }
42
+
43
+ metadata_artist = ""
44
+ metadata_title = ""
45
+
46
+ if "url" in extracted_info:
47
+ parsed_data["url"] = extracted_info["url"]
48
+ elif "webpage_url" in extracted_info:
49
+ parsed_data["url"] = extracted_info["webpage_url"]
50
+ else:
51
+ raise Exception(f"Failed to extract URL from input media metadata: {extracted_info}")
52
+
53
+ if "extractor_key" in extracted_info:
54
+ parsed_data["extractor"] = extracted_info["extractor_key"]
55
+ elif "ie_key" in extracted_info:
56
+ parsed_data["extractor"] = extracted_info["ie_key"]
57
+ else:
58
+ raise Exception(f"Failed to find extractor name from input media metadata: {extracted_info}")
59
+
60
+ if "id" in extracted_info:
61
+ parsed_data["media_id"] = extracted_info["id"]
62
+
63
+ # Example: "Artist - Title"
64
+ if "title" in extracted_info and "-" in extracted_info["title"]:
65
+ try:
66
+ metadata_artist, metadata_title = extracted_info["title"].split("-", 1)
67
+ metadata_artist = metadata_artist.strip()
68
+ metadata_title = metadata_title.strip()
69
+ except ValueError:
70
+ logger.warning(f"Could not split title '{extracted_info['title']}' on '-', using full title.")
71
+ metadata_title = extracted_info["title"].strip()
72
+ if "uploader" in extracted_info:
73
+ metadata_artist = extracted_info["uploader"]
74
+
75
+ elif "uploader" in extracted_info:
76
+ # Fallback to uploader as artist if title parsing fails
77
+ metadata_artist = extracted_info["uploader"]
78
+ if "title" in extracted_info:
79
+ metadata_title = extracted_info["title"].strip()
80
+
81
+ # If unable to parse, log an appropriate message
82
+ if not metadata_artist or not metadata_title:
83
+ logger.warning("Could not parse artist and title from the input media metadata.")
84
+
85
+ if not parsed_data["artist"] and metadata_artist:
86
+ logger.warning(f"Artist not provided as input, setting to {metadata_artist} from input media metadata...")
87
+ parsed_data["artist"] = metadata_artist
88
+
89
+ if not parsed_data["title"] and metadata_title:
90
+ logger.warning(f"Title not provided as input, setting to {metadata_title} from input media metadata...")
91
+ parsed_data["title"] = metadata_title
92
+
93
+ if persistent_artist:
94
+ logger.debug(
95
+ f"Resetting artist from {parsed_data['artist']} to persistent artist: {persistent_artist} for consistency while processing playlist..."
96
+ )
97
+ parsed_data["artist"] = persistent_artist
98
+
99
+ if parsed_data["artist"] and parsed_data["title"]:
100
+ logger.info(f"Extracted url: {parsed_data['url']}, artist: {parsed_data['artist']}, title: {parsed_data['title']}")
101
+ else:
102
+ logger.debug(extracted_info)
103
+ raise Exception("Failed to extract artist and title from the input media metadata.")
104
+
105
+ return parsed_data
Binary file
@@ -0,0 +1,18 @@
1
+ import re
2
+
3
+ def sanitize_filename(filename):
4
+ """Replace or remove characters that are unsafe for filenames."""
5
+ if filename is None:
6
+ return None
7
+ # Replace problematic characters with underscores
8
+ for char in ["\\", "/", ":", "*", "?", '"', "<", ">", "|"]:
9
+ filename = filename.replace(char, "_")
10
+ # Remove any trailing periods or spaces
11
+ filename = filename.rstrip(". ") # Added period here as well
12
+ # Remove any leading periods or spaces
13
+ filename = filename.lstrip(". ")
14
+ # Replace multiple underscores with a single one
15
+ filename = re.sub(r'_+', '_', filename)
16
+ # Replace multiple spaces with a single one
17
+ filename = re.sub(r' +', ' ', filename)
18
+ return filename