lyrics-transcriber 0.39.0__py3-none-any.whl → 0.40.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.
@@ -9,6 +9,7 @@ import os
9
9
  import zipfile
10
10
  import shutil
11
11
 
12
+ from lyrics_transcriber.output.cdgmaker.cdg import CDG_VISIBLE_WIDTH
12
13
  from lyrics_transcriber.output.cdgmaker.composer import KaraokeComposer
13
14
  from lyrics_transcriber.output.cdgmaker.render import get_wrapped_text
14
15
  from lyrics_transcriber.types import LyricsSegment
@@ -110,7 +111,7 @@ class CDGGenerator:
110
111
  # Convert time from seconds to centiseconds
111
112
  timestamp = int(word.start_time * 100)
112
113
  lyrics_data.append({"timestamp": timestamp, "text": word.text.upper()}) # CDG format expects uppercase text
113
- # self.logger.debug(f"Added lyric: timestamp {timestamp}, text '{word.text}'")
114
+ self.logger.debug(f"Added lyric: timestamp {timestamp}, text '{word.text}'")
114
115
 
115
116
  # Sort by timestamp to ensure correct order
116
117
  lyrics_data.sort(key=lambda x: x["timestamp"])
@@ -338,20 +339,20 @@ class CDGGenerator:
338
339
  formatted_lyrics = []
339
340
 
340
341
  for i, lyric in enumerate(lyrics_data):
341
- # self.logger.debug(f"Processing lyric {i}: timestamp {lyric['timestamp']}, text '{lyric['text']}'")
342
+ self.logger.debug(f"Processing lyric {i}: timestamp {lyric['timestamp']}, text '{lyric['text']}'")
342
343
 
343
344
  if i == 0 or lyric["timestamp"] - lyrics_data[i - 1]["timestamp"] >= cdg_styles["lead_in_threshold"]:
344
345
  lead_in_start = lyric["timestamp"] - cdg_styles["lead_in_total"]
345
- # self.logger.debug(f"Adding lead-in before lyric {i} at timestamp {lead_in_start}")
346
+ self.logger.debug(f"Adding lead-in before lyric {i} at timestamp {lead_in_start}")
346
347
  for j, symbol in enumerate(cdg_styles["lead_in_symbols"]):
347
348
  sync_time = lead_in_start + j * cdg_styles["lead_in_duration"]
348
349
  sync_times.append(sync_time)
349
350
  formatted_lyrics.append(symbol)
350
- # self.logger.debug(f" Added lead-in symbol {j+1}: '{symbol}' at {sync_time}")
351
+ self.logger.debug(f" Added lead-in symbol {j+1}: '{symbol}' at {sync_time}")
351
352
 
352
353
  sync_times.append(lyric["timestamp"])
353
354
  formatted_lyrics.append(lyric["text"])
354
- # self.logger.debug(f"Added lyric: '{lyric['text']}' at {lyric['timestamp']}")
355
+ self.logger.debug(f"Added lyric: '{lyric['text']}' at {lyric['timestamp']}")
355
356
 
356
357
  formatted_text = self.format_lyrics(
357
358
  formatted_lyrics,
@@ -473,24 +474,29 @@ class CDGGenerator:
473
474
  page_number = 1
474
475
 
475
476
  for i, text in enumerate(lyrics_data):
476
- # self.logger.debug(f"Processing text {i}: '{text}' (sync time: {sync_times[i]})")
477
+ self.logger.debug(f"format_lyrics: Processing text {i}: '{text}' (sync time: {sync_times[i]})")
477
478
 
478
479
  if text.startswith("/"):
479
480
  if current_line:
480
- wrapped_lines = get_wrapped_text(current_line.strip(), font, self.cdg_visible_width).split("\n")
481
+ wrapped_lines = get_wrapped_text(current_line.strip(), font, CDG_VISIBLE_WIDTH).split("\n")
481
482
  for wrapped_line in wrapped_lines:
482
483
  formatted_lyrics.append(wrapped_line)
483
484
  lines_on_page += 1
484
- # self.logger.debug(f"Added wrapped line: '{wrapped_line}'. Lines on page: {lines_on_page}")
485
+ self.logger.debug(f"format_lyrics: Added wrapped line: '{wrapped_line}'. Lines on page: {lines_on_page}")
486
+ # Add empty line after punctuation immediately
487
+ if wrapped_line.endswith(("!", "?", ".")) and not wrapped_line == "~":
488
+ formatted_lyrics.append("~")
489
+ lines_on_page += 1
490
+ self.logger.debug(f"format_lyrics: Added empty line after punctuation. Lines on page now: {lines_on_page}")
485
491
  if lines_on_page == 4:
486
492
  lines_on_page = 0
487
493
  page_number += 1
488
- # self.logger.debug(f"Page full. New page number: {page_number}")
494
+ self.logger.debug(f"format_lyrics: Page full. New page number: {page_number}")
489
495
  current_line = ""
490
496
  text = text[1:]
491
497
 
492
498
  current_line += text + " "
493
- # self.logger.debug(f"Current line: '{current_line}'")
499
+ self.logger.debug(f"format_lyrics: Current line: '{current_line}'")
494
500
 
495
501
  is_last_before_instrumental = any(
496
502
  inst["sync"] > sync_times[i] and (i == len(sync_times) - 1 or sync_times[i + 1] > inst["sync"]) for inst in instrumentals
@@ -498,33 +504,103 @@ class CDGGenerator:
498
504
 
499
505
  if is_last_before_instrumental or i == len(lyrics_data) - 1:
500
506
  if current_line:
501
- wrapped_lines = get_wrapped_text(current_line.strip(), font, self.cdg_visible_width).split("\n")
507
+ wrapped_lines = get_wrapped_text(current_line.strip(), font, CDG_VISIBLE_WIDTH).split("\n")
502
508
  for wrapped_line in wrapped_lines:
503
509
  formatted_lyrics.append(wrapped_line)
504
510
  lines_on_page += 1
505
- # self.logger.debug(f"Added wrapped line at end of section: '{wrapped_line}'. Lines on page: {lines_on_page}")
511
+ self.logger.debug(
512
+ f"format_lyrics: Added wrapped line at end of section: '{wrapped_line}'. Lines on page: {lines_on_page}"
513
+ )
506
514
  if lines_on_page == 4:
507
515
  lines_on_page = 0
508
516
  page_number += 1
509
- # self.logger.debug(f"Page full. New page number: {page_number}")
517
+ self.logger.debug(f"format_lyrics: Page full. New page number: {page_number}")
510
518
  current_line = ""
511
519
 
512
520
  if is_last_before_instrumental:
513
- blank_lines_needed = 4 - lines_on_page
514
- if blank_lines_needed < 4:
515
- formatted_lyrics.extend(["~"] * blank_lines_needed)
516
- # self.logger.debug(f"Added {blank_lines_needed} empty lines before instrumental. Lines on page was {lines_on_page}")
521
+ self.logger.debug(f"format_lyrics: is_last_before_instrumental: True lines_on_page: {lines_on_page}")
522
+ # Calculate remaining lines needed to reach next full page
523
+ remaining_lines = 4 - (lines_on_page % 4) if lines_on_page % 4 != 0 else 0
524
+ if remaining_lines > 0:
525
+ formatted_lyrics.extend(["~"] * remaining_lines)
526
+ self.logger.debug(f"format_lyrics: Added {remaining_lines} empty lines to complete current page")
527
+
528
+ # Reset the counter and increment page
517
529
  lines_on_page = 0
518
530
  page_number += 1
519
- # self.logger.debug(f"Reset lines_on_page to 0. New page number: {page_number}")
520
-
521
- final_lyrics = []
522
- for line in formatted_lyrics:
523
- final_lyrics.append(line)
524
- if line.endswith(("!", "?", ".")) and not line == "~":
525
- final_lyrics.append("~")
526
- # self.logger.debug("Added empty line after punctuation")
527
-
528
- result = "\n".join(final_lyrics)
529
- # self.logger.debug(f"Final formatted lyrics:\n{result}")
530
- return result
531
+ self.logger.debug(f"format_lyrics: Reset lines_on_page to 0. New page number: {page_number}")
532
+
533
+ return "\n".join(formatted_lyrics)
534
+
535
+ def generate_cdg_from_lrc(
536
+ self,
537
+ lrc_file: str,
538
+ audio_file: str,
539
+ title: str,
540
+ artist: str,
541
+ cdg_styles: dict,
542
+ ) -> Tuple[str, str, str]:
543
+ """Generate a CDG file from an LRC file and audio file.
544
+
545
+ Args:
546
+ lrc_file: Path to the LRC file
547
+ audio_file: Path to the audio file
548
+ title: Title of the song
549
+ artist: Artist name
550
+ cdg_styles: Dictionary containing CDG style parameters
551
+
552
+ Returns:
553
+ Tuple containing paths to (cdg_file, mp3_file, zip_file)
554
+ """
555
+ self._validate_and_setup_font(cdg_styles)
556
+
557
+ # Parse LRC file and convert to lyrics_data format
558
+ lyrics_data = self._parse_lrc(lrc_file)
559
+
560
+ toml_file = self._create_toml_file(
561
+ audio_file=audio_file,
562
+ title=title,
563
+ artist=artist,
564
+ lyrics_data=lyrics_data,
565
+ cdg_styles=cdg_styles,
566
+ )
567
+
568
+ try:
569
+ self._compose_cdg(toml_file)
570
+ output_zip = self._find_cdg_zip(artist, title)
571
+ self._extract_cdg_files(output_zip)
572
+
573
+ cdg_file = self._get_cdg_path(artist, title)
574
+ mp3_file = self._get_mp3_path(artist, title)
575
+
576
+ self._verify_output_files(cdg_file, mp3_file)
577
+
578
+ self.logger.info("CDG file generated successfully")
579
+ return cdg_file, mp3_file, output_zip
580
+
581
+ except Exception as e:
582
+ self.logger.error(f"Error composing CDG: {e}")
583
+ raise
584
+
585
+ def _parse_lrc(self, lrc_file: str) -> List[dict]:
586
+ """Parse LRC file and extract timestamps and lyrics."""
587
+ with open(lrc_file, "r", encoding="utf-8") as f:
588
+ content = f.read()
589
+
590
+ # Extract timestamps and lyrics
591
+ pattern = r"\[(\d{2}):(\d{2})\.(\d{3})\](\d+:)?(/?.*)"
592
+ matches = re.findall(pattern, content)
593
+
594
+ if not matches:
595
+ raise ValueError(f"No valid lyrics found in the LRC file: {lrc_file}")
596
+
597
+ lyrics = []
598
+ for match in matches:
599
+ minutes, seconds, milliseconds = map(int, match[:3])
600
+ timestamp = (minutes * 60 + seconds) * 100 + int(milliseconds / 10) # Convert to centiseconds
601
+ text = match[4].strip().upper()
602
+ if text: # Only add non-empty lyrics
603
+ lyrics.append({"timestamp": timestamp, "text": text})
604
+
605
+ self.logger.info(f"Found {len(lyrics)} lyric lines")
606
+ return lyrics
@@ -1097,27 +1097,16 @@ class KaraokeComposer:
1097
1097
  else:
1098
1098
  logger.debug("this instrumental did not wait for the previous " "line to finish")
1099
1099
 
1100
- logger.debug("purging all highlight/draw queues")
1100
+ logger.debug("_compose_lyric: Purging all highlight/draw queues")
1101
1101
  for st in lyric_states:
1102
- # If instrumental has waited for this syllable to end
1103
1102
  if instrumental.wait:
1104
- # There shouldn't be anything in the highlight queue
1105
- assert not st.highlight_queue
1106
- # If there's anything left in the draw queue
1103
+ if st.highlight_queue:
1104
+ logger.warning("_compose_lyric: Unexpected items in highlight queue when instrumental waited")
1107
1105
  if st.draw_queue:
1108
- # NOTE If the current lyric state has anything
1109
- # left in the draw queue, it should be the
1110
- # erasing of the current line.
1111
1106
  if st == state:
1112
- assert should_erase_this_line
1113
- # Queue everything left in the draw queue
1114
- # immediately
1115
- self.lyric_packet_indices.update(
1116
- range(
1117
- self.writer.packets_queued,
1118
- self.writer.packets_queued + len(st.draw_queue),
1119
- )
1120
- )
1107
+ logger.debug("_compose_lyric: Queueing remaining draw packets for current state")
1108
+ else:
1109
+ logger.warning("_compose_lyric: Unexpected items in draw queue for non-current state")
1121
1110
  self.writer.queue_packets(st.draw_queue)
1122
1111
 
1123
1112
  # Purge highlight/draw queues
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import logging
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from lyrics_transcriber.output.cdg import CDGGenerator
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def cli_main():
15
+ """Command-line interface entry point for the lrc2cdg tool."""
16
+ logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
17
+
18
+ parser = argparse.ArgumentParser(description="Convert LRC file to CDG")
19
+ parser.add_argument("lrc_file", help="Path to the LRC file")
20
+ parser.add_argument("audio_file", help="Path to the audio file")
21
+ parser.add_argument("--title", required=True, help="Title of the song")
22
+ parser.add_argument("--artist", required=True, help="Artist of the song")
23
+ parser.add_argument("--style_params_json", required=True, help="Path to JSON file containing CDG style configuration")
24
+
25
+ args = parser.parse_args()
26
+
27
+ try:
28
+ with open(args.style_params_json, "r") as f:
29
+ style_params = json.loads(f.read())
30
+ cdg_styles = style_params["cdg"]
31
+ except FileNotFoundError:
32
+ logger.error(f"Style configuration file not found: {args.style_params_json}")
33
+ sys.exit(1)
34
+ except json.JSONDecodeError as e:
35
+ logger.error(f"Invalid JSON in style configuration file: {e}")
36
+ sys.exit(1)
37
+
38
+ try:
39
+ output_dir = str(Path(args.lrc_file).parent)
40
+ generator = CDGGenerator(output_dir=output_dir, logger=logger)
41
+
42
+ cdg_file, mp3_file, zip_file = generator.generate_cdg_from_lrc(
43
+ lrc_file=args.lrc_file,
44
+ audio_file=args.audio_file,
45
+ title=args.title,
46
+ artist=args.artist,
47
+ cdg_styles=cdg_styles,
48
+ )
49
+
50
+ logger.info(f"Generated files:\nCDG: {cdg_file}\nMP3: {mp3_file}\nZIP: {zip_file}")
51
+
52
+ except ValueError as e:
53
+ logger.error(f"Invalid style configuration: {e}")
54
+ sys.exit(1)
55
+ except Exception as e:
56
+ logger.error(f"Error generating CDG: {e}")
57
+ sys.exit(1)
58
+
59
+
60
+ if __name__ == "__main__":
61
+ cli_main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lyrics-transcriber
3
- Version: 0.39.0
3
+ Version: 0.40.0
4
4
  Summary: Automatically create synchronised lyrics files in ASS and MidiCo LRC formats with word-level timestamps, using Whisper and lyrics from Genius and Spotify
5
5
  License: MIT
6
6
  Author: Andrew Beveridge
@@ -81,10 +81,10 @@ lyrics_transcriber/output/ass/lyrics_screen.py,sha256=gRzUsDMLEtZZPuv77xk7M0FzCp
81
81
  lyrics_transcriber/output/ass/section_detector.py,sha256=TsSf4E0fleC-Tzd5KK6q4m-wjGiu6TvGDtHdR6sUqvc,3922
82
82
  lyrics_transcriber/output/ass/section_screen.py,sha256=QeUaIeDXs_Es33W5aqyVSaZzMwUx-b60vbAww3aQfls,4185
83
83
  lyrics_transcriber/output/ass/style.py,sha256=ty3IGorlOZ_Q-TxeA02hNb5Pb0mA755dOb8bqKr1k7U,6880
84
- lyrics_transcriber/output/cdg.py,sha256=gWyFUAsG9xNTENWQdPTZDIWrr8jNHZgH2U4bA3iN9dM,21950
84
+ lyrics_transcriber/output/cdg.py,sha256=YKGqljyX4L7j3mg2Y3YfATIPSOszErVtdmyp8SjYnQk,24916
85
85
  lyrics_transcriber/output/cdgmaker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
86
86
  lyrics_transcriber/output/cdgmaker/cdg.py,sha256=nBqkw0JOois-NI27CkwHblLuBaoL-sHyJb2SntX7m8s,6733
87
- lyrics_transcriber/output/cdgmaker/composer.py,sha256=l04-wnUZMo5NUhgd5Ji-QqYwy-j3esqZCT-Qohll7bk,91250
87
+ lyrics_transcriber/output/cdgmaker/composer.py,sha256=gWziaLnCo8iV7Igp7yFn5oFUVctRDtkLlcfx6O-f98Y,90780
88
88
  lyrics_transcriber/output/cdgmaker/config.py,sha256=dOsOaPg9XawR3sWdTBoQiYn7urQWafV2KzedhI6BHYU,4043
89
89
  lyrics_transcriber/output/cdgmaker/images/instrumental.png,sha256=EKUcJJGj95ceNqw7M-O9ltX4HZIaCaSKjJucKVDTSb8,14834
90
90
  lyrics_transcriber/output/cdgmaker/images/intro.png,sha256=XeN6i8aKaQObfRwcgHT8ajQAJIDVjXEd7C5tu7DtriU,17603
@@ -112,6 +112,7 @@ lyrics_transcriber/output/fonts/arial.ttf,sha256=NcDzVZ2NtWnjbDEJW4pg1EFkPZX1kTn
112
112
  lyrics_transcriber/output/fonts/georgia.ttf,sha256=fQuyDGMrtZ6BoIhfVzvSFz9x9zIE3pBY_raM4DIicHI,142964
113
113
  lyrics_transcriber/output/fonts/verdana.ttf,sha256=lu0UlJyktzks_yNbnEHVXBJTgqu-DA08K53WaJfK4Ms,139640
114
114
  lyrics_transcriber/output/generator.py,sha256=TzgYjppsGKev5Fg-q7JtjTAiTkuKOBE8Ssaf6ZoAwQA,8294
115
+ lyrics_transcriber/output/lrc_to_cdg.py,sha256=2pi5tvreD_ADAR4RF5yVwj7OJ4Pf5Zo_EJ7rt4iH3k0,2063
115
116
  lyrics_transcriber/output/lyrics_file.py,sha256=_KQyQjCOMIwQdQ0115uEAUIjQWTRmShkSfQuINPKxaw,3741
116
117
  lyrics_transcriber/output/plain_text.py,sha256=3mYKq0BLYz1rGBD6ROjG2dn6BPuzbn5dxIQbWZVi4ao,3689
117
118
  lyrics_transcriber/output/segment_resizer.py,sha256=b553FCdcjYAl9T1IA5K6ya0pcn1-irD5spmxSc26wnI,17143
@@ -125,8 +126,8 @@ lyrics_transcriber/transcribers/audioshake.py,sha256=QzKGimVa6BovlvYFj35CbGpaGeP
125
126
  lyrics_transcriber/transcribers/base_transcriber.py,sha256=yPzUWPTCGmzE97H5Rz6g61e-qEGL77ZzUoiBOmswhts,5973
126
127
  lyrics_transcriber/transcribers/whisper.py,sha256=P0kas2_oX16MO1-Qy7U5gl5KQN-RuUIJZz7LsEFLUiE,12906
127
128
  lyrics_transcriber/types.py,sha256=xGf3hkTRcGZTTAjMVIev2i2DOU6co0QGpW8NxvaBQAA,16759
128
- lyrics_transcriber-0.39.0.dist-info/LICENSE,sha256=BiPihPDxhxIPEx6yAxVfAljD5Bhm_XG2teCbPEj_m0Y,1069
129
- lyrics_transcriber-0.39.0.dist-info/METADATA,sha256=9YYI3pAEcVVzJkjENCSchgEnp_M5MOUFrSjRhfpwVRk,5891
130
- lyrics_transcriber-0.39.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
131
- lyrics_transcriber-0.39.0.dist-info/entry_points.txt,sha256=SoGPp-kikJ9tPNF8vnjcnCyBrNWCW7AbaoZs1dIbXCo,162
132
- lyrics_transcriber-0.39.0.dist-info/RECORD,,
129
+ lyrics_transcriber-0.40.0.dist-info/LICENSE,sha256=BiPihPDxhxIPEx6yAxVfAljD5Bhm_XG2teCbPEj_m0Y,1069
130
+ lyrics_transcriber-0.40.0.dist-info/METADATA,sha256=hj-_pxAxvpcNC7XBPLbEvpPQYEDfHazvIOHrlXsWUOk,5891
131
+ lyrics_transcriber-0.40.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
132
+ lyrics_transcriber-0.40.0.dist-info/entry_points.txt,sha256=kcp-bSFkCACAEA0t166Kek0HpaJUXRo5SlF5tVrqNBU,216
133
+ lyrics_transcriber-0.40.0.dist-info/RECORD,,
@@ -1,5 +1,6 @@
1
1
  [console_scripts]
2
2
  cdgmaker=lyrics_transcriber.output.cdgmaker.composer:main
3
+ lrc2cdg=lyrics_transcriber.output.lrc_to_cdg:cli_main
3
4
  lyrics-transcriber=lyrics_transcriber.cli.cli_main:main
4
5
  test-cov=tests.conftest:main
5
6