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,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
|
karaoke_prep/metadata.py
ADDED
|
@@ -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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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
|