lyrics-transcriber 0.30.0__py3-none-any.whl → 0.32.1__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.
- lyrics_transcriber/__init__.py +2 -1
- lyrics_transcriber/cli/{main.py → cli_main.py} +47 -14
- lyrics_transcriber/core/config.py +35 -0
- lyrics_transcriber/core/controller.py +164 -166
- lyrics_transcriber/correction/anchor_sequence.py +471 -0
- lyrics_transcriber/correction/corrector.py +256 -0
- lyrics_transcriber/correction/handlers/__init__.py +0 -0
- lyrics_transcriber/correction/handlers/base.py +30 -0
- lyrics_transcriber/correction/handlers/extend_anchor.py +91 -0
- lyrics_transcriber/correction/handlers/levenshtein.py +147 -0
- lyrics_transcriber/correction/handlers/no_space_punct_match.py +98 -0
- lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +55 -0
- lyrics_transcriber/correction/handlers/repeat.py +71 -0
- lyrics_transcriber/correction/handlers/sound_alike.py +223 -0
- lyrics_transcriber/correction/handlers/syllables_match.py +182 -0
- lyrics_transcriber/correction/handlers/word_count_match.py +54 -0
- lyrics_transcriber/correction/handlers/word_operations.py +135 -0
- lyrics_transcriber/correction/phrase_analyzer.py +426 -0
- lyrics_transcriber/correction/text_utils.py +30 -0
- lyrics_transcriber/lyrics/base_lyrics_provider.py +125 -0
- lyrics_transcriber/lyrics/genius.py +73 -0
- lyrics_transcriber/lyrics/spotify.py +82 -0
- lyrics_transcriber/output/ass/__init__.py +21 -0
- lyrics_transcriber/output/{ass.py → ass/ass.py} +150 -690
- lyrics_transcriber/output/ass/ass_specs.txt +732 -0
- lyrics_transcriber/output/ass/config.py +37 -0
- lyrics_transcriber/output/ass/constants.py +23 -0
- lyrics_transcriber/output/ass/event.py +94 -0
- lyrics_transcriber/output/ass/formatters.py +132 -0
- lyrics_transcriber/output/ass/lyrics_line.py +219 -0
- lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
- lyrics_transcriber/output/ass/section_detector.py +89 -0
- lyrics_transcriber/output/ass/section_screen.py +106 -0
- lyrics_transcriber/output/ass/style.py +187 -0
- lyrics_transcriber/output/cdg.py +503 -0
- lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
- lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
- lyrics_transcriber/output/cdgmaker/composer.py +1919 -0
- lyrics_transcriber/output/cdgmaker/config.py +151 -0
- lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
- lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
- lyrics_transcriber/output/cdgmaker/pack.py +507 -0
- lyrics_transcriber/output/cdgmaker/render.py +346 -0
- lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
- lyrics_transcriber/output/cdgmaker/utils.py +132 -0
- lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
- lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
- lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/arial.ttf +0 -0
- lyrics_transcriber/output/fonts/georgia.ttf +0 -0
- lyrics_transcriber/output/fonts/verdana.ttf +0 -0
- lyrics_transcriber/output/generator.py +140 -171
- lyrics_transcriber/output/lyrics_file.py +102 -0
- lyrics_transcriber/output/plain_text.py +91 -0
- lyrics_transcriber/output/segment_resizer.py +416 -0
- lyrics_transcriber/output/subtitles.py +328 -302
- lyrics_transcriber/output/video.py +219 -0
- lyrics_transcriber/review/__init__.py +1 -0
- lyrics_transcriber/review/server.py +138 -0
- lyrics_transcriber/storage/dropbox.py +110 -134
- lyrics_transcriber/transcribers/audioshake.py +171 -105
- lyrics_transcriber/transcribers/base_transcriber.py +149 -0
- lyrics_transcriber/transcribers/whisper.py +267 -133
- lyrics_transcriber/types.py +454 -0
- {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/METADATA +14 -3
- lyrics_transcriber-0.32.1.dist-info/RECORD +86 -0
- {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/WHEEL +1 -1
- lyrics_transcriber-0.32.1.dist-info/entry_points.txt +4 -0
- lyrics_transcriber/core/corrector.py +0 -56
- lyrics_transcriber/core/fetcher.py +0 -143
- lyrics_transcriber/storage/tokens.py +0 -116
- lyrics_transcriber/transcribers/base.py +0 -31
- lyrics_transcriber-0.30.0.dist-info/RECORD +0 -22
- lyrics_transcriber-0.30.0.dist-info/entry_points.txt +0 -3
- {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/LICENSE +0 -0
@@ -0,0 +1,503 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import List, Optional, Tuple
|
3
|
+
import logging
|
4
|
+
import re
|
5
|
+
import toml
|
6
|
+
from pathlib import Path
|
7
|
+
from PIL import ImageFont
|
8
|
+
import os
|
9
|
+
import zipfile
|
10
|
+
import shutil
|
11
|
+
|
12
|
+
from lyrics_transcriber.output.cdgmaker.composer import KaraokeComposer
|
13
|
+
from lyrics_transcriber.output.cdgmaker.render import get_wrapped_text
|
14
|
+
from lyrics_transcriber.types import LyricsSegment
|
15
|
+
|
16
|
+
|
17
|
+
class CDGGenerator:
|
18
|
+
"""Generates CD+G (CD Graphics) format karaoke files."""
|
19
|
+
|
20
|
+
def __init__(self, output_dir: str, logger: Optional[logging.Logger] = None):
|
21
|
+
"""Initialize CDGGenerator.
|
22
|
+
|
23
|
+
Args:
|
24
|
+
output_dir: Directory where output files will be written
|
25
|
+
logger: Optional logger instance
|
26
|
+
"""
|
27
|
+
self.output_dir = output_dir
|
28
|
+
self.logger = logger or logging.getLogger(__name__)
|
29
|
+
self.cdg_visible_width = 280
|
30
|
+
|
31
|
+
def generate_cdg(
|
32
|
+
self,
|
33
|
+
segments: List[LyricsSegment],
|
34
|
+
audio_file: str,
|
35
|
+
title: str,
|
36
|
+
artist: str,
|
37
|
+
cdg_styles: dict,
|
38
|
+
) -> Tuple[str, str, str]:
|
39
|
+
"""Generate a CDG file from lyrics segments and audio file.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
segments: List of LyricsSegment objects containing timing and text
|
43
|
+
audio_file: Path to the audio file
|
44
|
+
title: Title of the song
|
45
|
+
artist: Artist name
|
46
|
+
cdg_styles: Dictionary containing CDG style parameters
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
Tuple containing paths to (cdg_file, mp3_file, zip_file)
|
50
|
+
"""
|
51
|
+
self._validate_and_setup_font(cdg_styles)
|
52
|
+
|
53
|
+
# Convert segments to the format expected by the rest of the code
|
54
|
+
lyrics_data = self._convert_segments_to_lyrics_data(segments)
|
55
|
+
|
56
|
+
toml_file = self._create_toml_file(
|
57
|
+
audio_file=audio_file,
|
58
|
+
title=title,
|
59
|
+
artist=artist,
|
60
|
+
lyrics_data=lyrics_data,
|
61
|
+
cdg_styles=cdg_styles,
|
62
|
+
)
|
63
|
+
|
64
|
+
try:
|
65
|
+
self._compose_cdg(toml_file)
|
66
|
+
output_zip = self._find_cdg_zip(artist, title)
|
67
|
+
self._extract_cdg_files(output_zip)
|
68
|
+
|
69
|
+
cdg_file = self._get_cdg_path(artist, title)
|
70
|
+
mp3_file = self._get_mp3_path(artist, title)
|
71
|
+
|
72
|
+
self._verify_output_files(cdg_file, mp3_file)
|
73
|
+
|
74
|
+
self.logger.info("CDG file generated successfully")
|
75
|
+
return cdg_file, mp3_file, output_zip
|
76
|
+
|
77
|
+
except Exception as e:
|
78
|
+
self.logger.error(f"Error composing CDG: {e}")
|
79
|
+
raise
|
80
|
+
|
81
|
+
def _convert_segments_to_lyrics_data(self, segments: List[LyricsSegment]) -> List[dict]:
|
82
|
+
"""Convert LyricsSegment objects to the format needed for CDG generation."""
|
83
|
+
lyrics_data = []
|
84
|
+
|
85
|
+
for segment in segments:
|
86
|
+
# Convert each word to a lyric entry
|
87
|
+
for word in segment.words:
|
88
|
+
# Convert time from seconds to centiseconds
|
89
|
+
timestamp = int(word.start_time * 100)
|
90
|
+
lyrics_data.append({"timestamp": timestamp, "text": word.text.upper()}) # CDG format expects uppercase text
|
91
|
+
# self.logger.debug(f"Added lyric: timestamp {timestamp}, text '{word.text}'")
|
92
|
+
|
93
|
+
# Sort by timestamp to ensure correct order
|
94
|
+
lyrics_data.sort(key=lambda x: x["timestamp"])
|
95
|
+
return lyrics_data
|
96
|
+
|
97
|
+
def _create_toml_file(
|
98
|
+
self,
|
99
|
+
audio_file: str,
|
100
|
+
title: str,
|
101
|
+
artist: str,
|
102
|
+
lyrics_data: List[dict],
|
103
|
+
cdg_styles: dict,
|
104
|
+
) -> str:
|
105
|
+
"""Create TOML configuration file for CDG generation."""
|
106
|
+
toml_file = os.path.join(self.output_dir, f"{artist} - {title} (Karaoke CDG).toml")
|
107
|
+
self.logger.debug(f"Generating TOML file: {toml_file}")
|
108
|
+
|
109
|
+
self.generate_toml(
|
110
|
+
audio_file=audio_file,
|
111
|
+
title=title,
|
112
|
+
artist=artist,
|
113
|
+
lyrics_data=lyrics_data,
|
114
|
+
output_file=toml_file,
|
115
|
+
cdg_styles=cdg_styles,
|
116
|
+
)
|
117
|
+
return toml_file
|
118
|
+
|
119
|
+
def generate_toml(
|
120
|
+
self,
|
121
|
+
audio_file: str,
|
122
|
+
title: str,
|
123
|
+
artist: str,
|
124
|
+
lyrics_data: List[dict],
|
125
|
+
output_file: str,
|
126
|
+
cdg_styles: dict,
|
127
|
+
) -> None:
|
128
|
+
"""Generate a TOML configuration file for CDG creation."""
|
129
|
+
audio_file = os.path.abspath(audio_file)
|
130
|
+
self.logger.debug(f"Using absolute audio file path: {audio_file}")
|
131
|
+
|
132
|
+
self._validate_cdg_styles(cdg_styles)
|
133
|
+
instrumentals = self._detect_instrumentals(lyrics_data, cdg_styles)
|
134
|
+
sync_times, formatted_lyrics = self._format_lyrics_data(lyrics_data, instrumentals, cdg_styles)
|
135
|
+
|
136
|
+
toml_data = self._create_toml_data(
|
137
|
+
title=title,
|
138
|
+
artist=artist,
|
139
|
+
audio_file=audio_file,
|
140
|
+
output_name=f"{artist} - {title} (Karaoke CDG)",
|
141
|
+
sync_times=sync_times,
|
142
|
+
instrumentals=instrumentals,
|
143
|
+
formatted_lyrics=formatted_lyrics,
|
144
|
+
cdg_styles=cdg_styles,
|
145
|
+
)
|
146
|
+
|
147
|
+
self._write_toml_file(toml_data, output_file)
|
148
|
+
|
149
|
+
def _validate_and_setup_font(self, cdg_styles: dict) -> None:
|
150
|
+
"""Validate and set up font path in CDG styles."""
|
151
|
+
if not cdg_styles.get("font_path"):
|
152
|
+
return
|
153
|
+
|
154
|
+
if not os.path.isabs(cdg_styles["font_path"]) and not os.path.exists(cdg_styles["font_path"]):
|
155
|
+
package_font_path = os.path.join(os.path.dirname(__file__), "fonts", cdg_styles["font_path"])
|
156
|
+
if os.path.exists(package_font_path):
|
157
|
+
cdg_styles["font_path"] = package_font_path
|
158
|
+
self.logger.debug(f"Found font in package fonts directory: {cdg_styles['font_path']}")
|
159
|
+
else:
|
160
|
+
self.logger.warning(
|
161
|
+
f"Font file {cdg_styles['font_path']} not found in package fonts directory {package_font_path}, will use default font"
|
162
|
+
)
|
163
|
+
cdg_styles["font_path"] = None
|
164
|
+
|
165
|
+
def _compose_cdg(self, toml_file: str) -> None:
|
166
|
+
"""Compose CDG using KaraokeComposer."""
|
167
|
+
kc = KaraokeComposer.from_file(toml_file)
|
168
|
+
kc.compose()
|
169
|
+
|
170
|
+
def _find_cdg_zip(self, artist: str, title: str) -> str:
|
171
|
+
"""Find the generated CDG ZIP file."""
|
172
|
+
expected_zip = f"{artist} - {title} (Karaoke CDG).zip"
|
173
|
+
output_zip = os.path.join(self.output_dir, expected_zip)
|
174
|
+
|
175
|
+
self.logger.info(f"Looking for CDG ZIP file in output directory: {output_zip}")
|
176
|
+
|
177
|
+
if os.path.isfile(output_zip):
|
178
|
+
self.logger.info(f"Found CDG ZIP file: {output_zip}")
|
179
|
+
return output_zip
|
180
|
+
|
181
|
+
self.logger.error("Failed to find CDG ZIP file. Output directory contents:")
|
182
|
+
for file in os.listdir(self.output_dir):
|
183
|
+
self.logger.error(f" - {file}")
|
184
|
+
raise FileNotFoundError(f"CDG ZIP file not found: {output_zip}")
|
185
|
+
|
186
|
+
def _extract_cdg_files(self, zip_path: str) -> None:
|
187
|
+
"""Extract files from the CDG ZIP."""
|
188
|
+
self.logger.info(f"Extracting CDG ZIP file: {zip_path}")
|
189
|
+
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
190
|
+
zip_ref.extractall(self.output_dir)
|
191
|
+
|
192
|
+
def _get_cdg_path(self, artist: str, title: str) -> str:
|
193
|
+
"""Get the path to the CDG file."""
|
194
|
+
return os.path.join(self.output_dir, f"{artist} - {title} (Karaoke CDG).cdg")
|
195
|
+
|
196
|
+
def _get_mp3_path(self, artist: str, title: str) -> str:
|
197
|
+
"""Get the path to the MP3 file."""
|
198
|
+
return os.path.join(self.output_dir, f"{artist} - {title} (Karaoke CDG).mp3")
|
199
|
+
|
200
|
+
def _verify_output_files(self, cdg_file: str, mp3_file: str) -> None:
|
201
|
+
"""Verify that the required output files exist."""
|
202
|
+
if not os.path.isfile(cdg_file):
|
203
|
+
raise FileNotFoundError(f"CDG file not found after extraction: {cdg_file}")
|
204
|
+
if not os.path.isfile(mp3_file):
|
205
|
+
raise FileNotFoundError(f"MP3 file not found after extraction: {mp3_file}")
|
206
|
+
|
207
|
+
def detect_instrumentals(
|
208
|
+
self,
|
209
|
+
lyrics_data,
|
210
|
+
line_tile_height,
|
211
|
+
instrumental_font_color,
|
212
|
+
instrumental_background,
|
213
|
+
instrumental_transition,
|
214
|
+
instrumental_gap_threshold,
|
215
|
+
instrumental_text,
|
216
|
+
):
|
217
|
+
instrumentals = []
|
218
|
+
for i in range(len(lyrics_data) - 1):
|
219
|
+
current_end = lyrics_data[i]["timestamp"]
|
220
|
+
next_start = lyrics_data[i + 1]["timestamp"]
|
221
|
+
gap = next_start - current_end
|
222
|
+
if gap >= instrumental_gap_threshold:
|
223
|
+
instrumental_start = current_end + 200 # Add 2 seconds (200 centiseconds) delay
|
224
|
+
instrumental_duration = (gap - 200) // 100 # Convert to seconds
|
225
|
+
instrumentals.append(
|
226
|
+
{
|
227
|
+
"sync": instrumental_start,
|
228
|
+
"wait": True,
|
229
|
+
"text": f"{instrumental_text}\n{instrumental_duration} seconds\n",
|
230
|
+
"text_align": "center",
|
231
|
+
"text_placement": "bottom middle",
|
232
|
+
"line_tile_height": line_tile_height,
|
233
|
+
"fill": instrumental_font_color,
|
234
|
+
"stroke": "",
|
235
|
+
"image": instrumental_background,
|
236
|
+
"transition": instrumental_transition,
|
237
|
+
}
|
238
|
+
)
|
239
|
+
self.logger.info(
|
240
|
+
f"Detected instrumental: Gap of {gap} cs, starting at {instrumental_start} cs, duration {instrumental_duration} seconds"
|
241
|
+
)
|
242
|
+
|
243
|
+
self.logger.info(f"Total instrumentals detected: {len(instrumentals)}")
|
244
|
+
return instrumentals
|
245
|
+
|
246
|
+
def _validate_cdg_styles(self, cdg_styles: dict) -> None:
|
247
|
+
"""Validate required style parameters are present."""
|
248
|
+
required_styles = {
|
249
|
+
"title_color",
|
250
|
+
"artist_color",
|
251
|
+
"background_color",
|
252
|
+
"border_color",
|
253
|
+
"font_path",
|
254
|
+
"font_size",
|
255
|
+
"stroke_width",
|
256
|
+
"stroke_style",
|
257
|
+
"active_fill",
|
258
|
+
"active_stroke",
|
259
|
+
"inactive_fill",
|
260
|
+
"inactive_stroke",
|
261
|
+
"title_screen_background",
|
262
|
+
"instrumental_background",
|
263
|
+
"instrumental_transition",
|
264
|
+
"instrumental_font_color",
|
265
|
+
"title_screen_transition",
|
266
|
+
"row",
|
267
|
+
"line_tile_height",
|
268
|
+
"lines_per_page",
|
269
|
+
"clear_mode",
|
270
|
+
"sync_offset",
|
271
|
+
"instrumental_gap_threshold",
|
272
|
+
"instrumental_text",
|
273
|
+
"lead_in_threshold",
|
274
|
+
"lead_in_symbols",
|
275
|
+
"lead_in_duration",
|
276
|
+
"lead_in_total",
|
277
|
+
"title_artist_gap",
|
278
|
+
"intro_duration_seconds",
|
279
|
+
"first_syllable_buffer_seconds",
|
280
|
+
"outro_background",
|
281
|
+
"outro_transition",
|
282
|
+
"outro_text_line1",
|
283
|
+
"outro_text_line2",
|
284
|
+
"outro_line1_color",
|
285
|
+
"outro_line2_color",
|
286
|
+
"outro_line1_line2_gap",
|
287
|
+
}
|
288
|
+
|
289
|
+
missing_styles = required_styles - set(cdg_styles.keys())
|
290
|
+
if missing_styles:
|
291
|
+
raise ValueError(f"Missing required style parameters: {', '.join(missing_styles)}")
|
292
|
+
|
293
|
+
def _detect_instrumentals(self, lyrics_data: List[dict], cdg_styles: dict) -> List[dict]:
|
294
|
+
"""Detect instrumental sections in lyrics."""
|
295
|
+
return self.detect_instrumentals(
|
296
|
+
lyrics_data=lyrics_data,
|
297
|
+
line_tile_height=cdg_styles["line_tile_height"],
|
298
|
+
instrumental_font_color=cdg_styles["instrumental_font_color"],
|
299
|
+
instrumental_background=cdg_styles["instrumental_background"],
|
300
|
+
instrumental_transition=cdg_styles["instrumental_transition"],
|
301
|
+
instrumental_gap_threshold=cdg_styles["instrumental_gap_threshold"],
|
302
|
+
instrumental_text=cdg_styles["instrumental_text"],
|
303
|
+
)
|
304
|
+
|
305
|
+
def _format_lyrics_data(self, lyrics_data: List[dict], instrumentals: List[dict], cdg_styles: dict) -> tuple[List[int], List[str]]:
|
306
|
+
"""Format lyrics data with lead-in symbols and handle line wrapping.
|
307
|
+
|
308
|
+
Returns:
|
309
|
+
tuple: (sync_times, formatted_lyrics) where sync_times includes lead-in timings
|
310
|
+
"""
|
311
|
+
sync_times = []
|
312
|
+
formatted_lyrics = []
|
313
|
+
|
314
|
+
for i, lyric in enumerate(lyrics_data):
|
315
|
+
# self.logger.debug(f"Processing lyric {i}: timestamp {lyric['timestamp']}, text '{lyric['text']}'")
|
316
|
+
|
317
|
+
if i == 0 or lyric["timestamp"] - lyrics_data[i - 1]["timestamp"] >= cdg_styles["lead_in_threshold"]:
|
318
|
+
lead_in_start = lyric["timestamp"] - cdg_styles["lead_in_total"]
|
319
|
+
# self.logger.debug(f"Adding lead-in before lyric {i} at timestamp {lead_in_start}")
|
320
|
+
for j, symbol in enumerate(cdg_styles["lead_in_symbols"]):
|
321
|
+
sync_time = lead_in_start + j * cdg_styles["lead_in_duration"]
|
322
|
+
sync_times.append(sync_time)
|
323
|
+
formatted_lyrics.append(symbol)
|
324
|
+
# self.logger.debug(f" Added lead-in symbol {j+1}: '{symbol}' at {sync_time}")
|
325
|
+
|
326
|
+
sync_times.append(lyric["timestamp"])
|
327
|
+
formatted_lyrics.append(lyric["text"])
|
328
|
+
# self.logger.debug(f"Added lyric: '{lyric['text']}' at {lyric['timestamp']}")
|
329
|
+
|
330
|
+
formatted_text = self.format_lyrics(
|
331
|
+
formatted_lyrics,
|
332
|
+
instrumentals,
|
333
|
+
sync_times,
|
334
|
+
font_path=cdg_styles["font_path"],
|
335
|
+
font_size=cdg_styles["font_size"],
|
336
|
+
)
|
337
|
+
|
338
|
+
return sync_times, formatted_text
|
339
|
+
|
340
|
+
def _create_toml_data(
|
341
|
+
self,
|
342
|
+
title: str,
|
343
|
+
artist: str,
|
344
|
+
audio_file: str,
|
345
|
+
output_name: str,
|
346
|
+
sync_times: List[int],
|
347
|
+
instrumentals: List[dict],
|
348
|
+
formatted_lyrics: List[str],
|
349
|
+
cdg_styles: dict,
|
350
|
+
) -> dict:
|
351
|
+
"""Create TOML data structure."""
|
352
|
+
return {
|
353
|
+
"title": title,
|
354
|
+
"artist": artist,
|
355
|
+
"file": audio_file,
|
356
|
+
"outname": output_name,
|
357
|
+
"clear_mode": cdg_styles["clear_mode"],
|
358
|
+
"sync_offset": cdg_styles["sync_offset"],
|
359
|
+
"background": cdg_styles["background_color"],
|
360
|
+
"border": cdg_styles["border_color"],
|
361
|
+
"font": cdg_styles["font_path"],
|
362
|
+
"font_size": cdg_styles["font_size"],
|
363
|
+
"stroke_width": cdg_styles["stroke_width"],
|
364
|
+
"stroke_style": cdg_styles["stroke_style"],
|
365
|
+
"singers": [
|
366
|
+
{
|
367
|
+
"active_fill": cdg_styles["active_fill"],
|
368
|
+
"active_stroke": cdg_styles["active_stroke"],
|
369
|
+
"inactive_fill": cdg_styles["inactive_fill"],
|
370
|
+
"inactive_stroke": cdg_styles["inactive_stroke"],
|
371
|
+
}
|
372
|
+
],
|
373
|
+
"lyrics": [
|
374
|
+
{
|
375
|
+
"singer": 1,
|
376
|
+
"sync": sync_times,
|
377
|
+
"row": cdg_styles["row"],
|
378
|
+
"line_tile_height": cdg_styles["line_tile_height"],
|
379
|
+
"lines_per_page": cdg_styles["lines_per_page"],
|
380
|
+
"text": formatted_lyrics,
|
381
|
+
}
|
382
|
+
],
|
383
|
+
"title_color": cdg_styles["title_color"],
|
384
|
+
"artist_color": cdg_styles["artist_color"],
|
385
|
+
"title_screen_background": cdg_styles["title_screen_background"],
|
386
|
+
"title_screen_transition": cdg_styles["title_screen_transition"],
|
387
|
+
"instrumentals": instrumentals,
|
388
|
+
"intro_duration_seconds": cdg_styles["intro_duration_seconds"],
|
389
|
+
"first_syllable_buffer_seconds": cdg_styles["first_syllable_buffer_seconds"],
|
390
|
+
"outro_background": cdg_styles["outro_background"],
|
391
|
+
"outro_transition": cdg_styles["outro_transition"],
|
392
|
+
"outro_text_line1": cdg_styles["outro_text_line1"],
|
393
|
+
"outro_text_line2": cdg_styles["outro_text_line2"],
|
394
|
+
"outro_line1_color": cdg_styles["outro_line1_color"],
|
395
|
+
"outro_line2_color": cdg_styles["outro_line2_color"],
|
396
|
+
"outro_line1_line2_gap": cdg_styles["outro_line1_line2_gap"],
|
397
|
+
}
|
398
|
+
|
399
|
+
def _write_toml_file(self, toml_data: dict, output_file: str) -> None:
|
400
|
+
"""Write TOML data to file."""
|
401
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
402
|
+
toml.dump(toml_data, f)
|
403
|
+
self.logger.info(f"TOML file generated: {output_file}")
|
404
|
+
|
405
|
+
def get_font(self, font_path=None, font_size=18):
|
406
|
+
try:
|
407
|
+
return ImageFont.truetype(font_path, font_size) if font_path else ImageFont.load_default()
|
408
|
+
except IOError:
|
409
|
+
self.logger.warning(f"Font file {font_path} not found. Using default font.")
|
410
|
+
return ImageFont.load_default()
|
411
|
+
|
412
|
+
def get_text_width(self, text, font):
|
413
|
+
return font.getmask(text).getbbox()[2]
|
414
|
+
|
415
|
+
def wrap_text(self, text, max_width, font):
|
416
|
+
words = text.split()
|
417
|
+
lines = []
|
418
|
+
current_line = []
|
419
|
+
current_width = 0
|
420
|
+
|
421
|
+
for word in words:
|
422
|
+
word_width = self.get_text_width(word, font)
|
423
|
+
if current_width + word_width <= max_width:
|
424
|
+
current_line.append(word)
|
425
|
+
current_width += word_width + self.get_text_width(" ", font)
|
426
|
+
else:
|
427
|
+
if current_line:
|
428
|
+
lines.append(" ".join(current_line))
|
429
|
+
self.logger.debug(f"Wrapped line: {' '.join(current_line)}")
|
430
|
+
current_line = [word]
|
431
|
+
current_width = word_width
|
432
|
+
|
433
|
+
if current_line:
|
434
|
+
lines.append(" ".join(current_line))
|
435
|
+
self.logger.debug(f"Wrapped line: {' '.join(current_line)}")
|
436
|
+
|
437
|
+
return lines
|
438
|
+
|
439
|
+
def format_lyrics(self, lyrics_data, instrumentals, sync_times, font_path=None, font_size=18):
|
440
|
+
formatted_lyrics = []
|
441
|
+
font = self.get_font(font_path, font_size)
|
442
|
+
self.logger.debug(f"Using font: {font}")
|
443
|
+
|
444
|
+
current_line = ""
|
445
|
+
lines_on_page = 0
|
446
|
+
page_number = 1
|
447
|
+
|
448
|
+
for i, text in enumerate(lyrics_data):
|
449
|
+
# self.logger.debug(f"Processing text {i}: '{text}' (sync time: {sync_times[i]})")
|
450
|
+
|
451
|
+
if text.startswith("/"):
|
452
|
+
if current_line:
|
453
|
+
wrapped_lines = get_wrapped_text(current_line.strip(), font, self.cdg_visible_width).split("\n")
|
454
|
+
for wrapped_line in wrapped_lines:
|
455
|
+
formatted_lyrics.append(wrapped_line)
|
456
|
+
lines_on_page += 1
|
457
|
+
# self.logger.debug(f"Added wrapped line: '{wrapped_line}'. Lines on page: {lines_on_page}")
|
458
|
+
if lines_on_page == 4:
|
459
|
+
lines_on_page = 0
|
460
|
+
page_number += 1
|
461
|
+
# self.logger.debug(f"Page full. New page number: {page_number}")
|
462
|
+
current_line = ""
|
463
|
+
text = text[1:]
|
464
|
+
|
465
|
+
current_line += text + " "
|
466
|
+
# self.logger.debug(f"Current line: '{current_line}'")
|
467
|
+
|
468
|
+
is_last_before_instrumental = any(
|
469
|
+
inst["sync"] > sync_times[i] and (i == len(sync_times) - 1 or sync_times[i + 1] > inst["sync"]) for inst in instrumentals
|
470
|
+
)
|
471
|
+
|
472
|
+
if is_last_before_instrumental or i == len(lyrics_data) - 1:
|
473
|
+
if current_line:
|
474
|
+
wrapped_lines = get_wrapped_text(current_line.strip(), font, self.cdg_visible_width).split("\n")
|
475
|
+
for wrapped_line in wrapped_lines:
|
476
|
+
formatted_lyrics.append(wrapped_line)
|
477
|
+
lines_on_page += 1
|
478
|
+
# self.logger.debug(f"Added wrapped line at end of section: '{wrapped_line}'. Lines on page: {lines_on_page}")
|
479
|
+
if lines_on_page == 4:
|
480
|
+
lines_on_page = 0
|
481
|
+
page_number += 1
|
482
|
+
# self.logger.debug(f"Page full. New page number: {page_number}")
|
483
|
+
current_line = ""
|
484
|
+
|
485
|
+
if is_last_before_instrumental:
|
486
|
+
blank_lines_needed = 4 - lines_on_page
|
487
|
+
if blank_lines_needed < 4:
|
488
|
+
formatted_lyrics.extend(["~"] * blank_lines_needed)
|
489
|
+
# self.logger.debug(f"Added {blank_lines_needed} empty lines before instrumental. Lines on page was {lines_on_page}")
|
490
|
+
lines_on_page = 0
|
491
|
+
page_number += 1
|
492
|
+
# self.logger.debug(f"Reset lines_on_page to 0. New page number: {page_number}")
|
493
|
+
|
494
|
+
final_lyrics = []
|
495
|
+
for line in formatted_lyrics:
|
496
|
+
final_lyrics.append(line)
|
497
|
+
if line.endswith(("!", "?", ".")) and not line == "~":
|
498
|
+
final_lyrics.append("~")
|
499
|
+
# self.logger.debug("Added empty line after punctuation")
|
500
|
+
|
501
|
+
result = "\n".join(final_lyrics)
|
502
|
+
# self.logger.debug(f"Final formatted lyrics:\n{result}")
|
503
|
+
return result
|
File without changes
|