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.
Files changed (88) hide show
  1. lyrics_transcriber/__init__.py +2 -1
  2. lyrics_transcriber/cli/{main.py → cli_main.py} +47 -14
  3. lyrics_transcriber/core/config.py +35 -0
  4. lyrics_transcriber/core/controller.py +164 -166
  5. lyrics_transcriber/correction/anchor_sequence.py +471 -0
  6. lyrics_transcriber/correction/corrector.py +256 -0
  7. lyrics_transcriber/correction/handlers/__init__.py +0 -0
  8. lyrics_transcriber/correction/handlers/base.py +30 -0
  9. lyrics_transcriber/correction/handlers/extend_anchor.py +91 -0
  10. lyrics_transcriber/correction/handlers/levenshtein.py +147 -0
  11. lyrics_transcriber/correction/handlers/no_space_punct_match.py +98 -0
  12. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +55 -0
  13. lyrics_transcriber/correction/handlers/repeat.py +71 -0
  14. lyrics_transcriber/correction/handlers/sound_alike.py +223 -0
  15. lyrics_transcriber/correction/handlers/syllables_match.py +182 -0
  16. lyrics_transcriber/correction/handlers/word_count_match.py +54 -0
  17. lyrics_transcriber/correction/handlers/word_operations.py +135 -0
  18. lyrics_transcriber/correction/phrase_analyzer.py +426 -0
  19. lyrics_transcriber/correction/text_utils.py +30 -0
  20. lyrics_transcriber/lyrics/base_lyrics_provider.py +125 -0
  21. lyrics_transcriber/lyrics/genius.py +73 -0
  22. lyrics_transcriber/lyrics/spotify.py +82 -0
  23. lyrics_transcriber/output/ass/__init__.py +21 -0
  24. lyrics_transcriber/output/{ass.py → ass/ass.py} +150 -690
  25. lyrics_transcriber/output/ass/ass_specs.txt +732 -0
  26. lyrics_transcriber/output/ass/config.py +37 -0
  27. lyrics_transcriber/output/ass/constants.py +23 -0
  28. lyrics_transcriber/output/ass/event.py +94 -0
  29. lyrics_transcriber/output/ass/formatters.py +132 -0
  30. lyrics_transcriber/output/ass/lyrics_line.py +219 -0
  31. lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
  32. lyrics_transcriber/output/ass/section_detector.py +89 -0
  33. lyrics_transcriber/output/ass/section_screen.py +106 -0
  34. lyrics_transcriber/output/ass/style.py +187 -0
  35. lyrics_transcriber/output/cdg.py +503 -0
  36. lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
  37. lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
  38. lyrics_transcriber/output/cdgmaker/composer.py +1919 -0
  39. lyrics_transcriber/output/cdgmaker/config.py +151 -0
  40. lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
  41. lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
  42. lyrics_transcriber/output/cdgmaker/pack.py +507 -0
  43. lyrics_transcriber/output/cdgmaker/render.py +346 -0
  44. lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
  45. lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
  46. lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
  47. lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
  48. lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
  49. lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
  50. lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
  51. lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
  52. lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
  53. lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
  54. lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
  55. lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
  56. lyrics_transcriber/output/cdgmaker/utils.py +132 -0
  57. lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
  58. lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
  59. lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
  60. lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
  61. lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
  62. lyrics_transcriber/output/fonts/arial.ttf +0 -0
  63. lyrics_transcriber/output/fonts/georgia.ttf +0 -0
  64. lyrics_transcriber/output/fonts/verdana.ttf +0 -0
  65. lyrics_transcriber/output/generator.py +140 -171
  66. lyrics_transcriber/output/lyrics_file.py +102 -0
  67. lyrics_transcriber/output/plain_text.py +91 -0
  68. lyrics_transcriber/output/segment_resizer.py +416 -0
  69. lyrics_transcriber/output/subtitles.py +328 -302
  70. lyrics_transcriber/output/video.py +219 -0
  71. lyrics_transcriber/review/__init__.py +1 -0
  72. lyrics_transcriber/review/server.py +138 -0
  73. lyrics_transcriber/storage/dropbox.py +110 -134
  74. lyrics_transcriber/transcribers/audioshake.py +171 -105
  75. lyrics_transcriber/transcribers/base_transcriber.py +149 -0
  76. lyrics_transcriber/transcribers/whisper.py +267 -133
  77. lyrics_transcriber/types.py +454 -0
  78. {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/METADATA +14 -3
  79. lyrics_transcriber-0.32.1.dist-info/RECORD +86 -0
  80. {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/WHEEL +1 -1
  81. lyrics_transcriber-0.32.1.dist-info/entry_points.txt +4 -0
  82. lyrics_transcriber/core/corrector.py +0 -56
  83. lyrics_transcriber/core/fetcher.py +0 -143
  84. lyrics_transcriber/storage/tokens.py +0 -116
  85. lyrics_transcriber/transcribers/base.py +0 -31
  86. lyrics_transcriber-0.30.0.dist-info/RECORD +0 -22
  87. lyrics_transcriber-0.30.0.dist-info/entry_points.txt +0 -3
  88. {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