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.
- lyrics_transcriber/output/cdg.py +105 -29
- lyrics_transcriber/output/cdgmaker/composer.py +6 -17
- lyrics_transcriber/output/lrc_to_cdg.py +61 -0
- {lyrics_transcriber-0.39.0.dist-info → lyrics_transcriber-0.40.0.dist-info}/METADATA +1 -1
- {lyrics_transcriber-0.39.0.dist-info → lyrics_transcriber-0.40.0.dist-info}/RECORD +8 -7
- {lyrics_transcriber-0.39.0.dist-info → lyrics_transcriber-0.40.0.dist-info}/entry_points.txt +1 -0
- {lyrics_transcriber-0.39.0.dist-info → lyrics_transcriber-0.40.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.39.0.dist-info → lyrics_transcriber-0.40.0.dist-info}/WHEEL +0 -0
lyrics_transcriber/output/cdg.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
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
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
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("
|
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
|
-
|
1105
|
-
|
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
|
-
|
1113
|
-
|
1114
|
-
|
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.
|
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=
|
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=
|
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.
|
129
|
-
lyrics_transcriber-0.
|
130
|
-
lyrics_transcriber-0.
|
131
|
-
lyrics_transcriber-0.
|
132
|
-
lyrics_transcriber-0.
|
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,,
|
File without changes
|
File without changes
|