lyrics-transcriber 0.39.0__py3-none-any.whl → 0.41.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/cli/cli_main.py +7 -0
- lyrics_transcriber/core/config.py +1 -0
- lyrics_transcriber/output/cdg.py +112 -36
- lyrics_transcriber/output/cdgmaker/composer.py +6 -17
- lyrics_transcriber/output/generator.py +7 -6
- lyrics_transcriber/output/lrc_to_cdg.py +61 -0
- lyrics_transcriber/output/subtitles.py +29 -1
- {lyrics_transcriber-0.39.0.dist-info → lyrics_transcriber-0.41.0.dist-info}/METADATA +1 -1
- {lyrics_transcriber-0.39.0.dist-info → lyrics_transcriber-0.41.0.dist-info}/RECORD +12 -11
- {lyrics_transcriber-0.39.0.dist-info → lyrics_transcriber-0.41.0.dist-info}/entry_points.txt +1 -0
- {lyrics_transcriber-0.39.0.dist-info → lyrics_transcriber-0.41.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.39.0.dist-info → lyrics_transcriber-0.41.0.dist-info}/WHEEL +0 -0
@@ -68,6 +68,12 @@ def create_arg_parser() -> argparse.ArgumentParser:
|
|
68
68
|
type=Path,
|
69
69
|
help="JSON file containing output style configurations for CDG and video generation",
|
70
70
|
)
|
71
|
+
output_group.add_argument(
|
72
|
+
"--subtitle_offset",
|
73
|
+
type=int,
|
74
|
+
default=0,
|
75
|
+
help="Offset subtitle timing by N milliseconds (positive or negative). Default: 0",
|
76
|
+
)
|
71
77
|
|
72
78
|
# Feature control group
|
73
79
|
feature_group = parser.add_argument_group("Feature Control")
|
@@ -143,6 +149,7 @@ def create_configs(args: argparse.Namespace, env_config: Dict[str, str]) -> tupl
|
|
143
149
|
output_dir=str(args.output_dir) if args.output_dir else os.getcwd(),
|
144
150
|
cache_dir=str(args.cache_dir),
|
145
151
|
video_resolution=args.video_resolution,
|
152
|
+
subtitle_offset_ms=args.subtitle_offset,
|
146
153
|
fetch_lyrics=not args.skip_lyrics_fetch,
|
147
154
|
run_transcription=not args.skip_transcription,
|
148
155
|
run_correction=not args.skip_correction,
|
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"])
|
@@ -125,7 +126,7 @@ class CDGGenerator:
|
|
125
126
|
cdg_styles: dict,
|
126
127
|
) -> str:
|
127
128
|
"""Create TOML configuration file for CDG generation."""
|
128
|
-
safe_filename = self._get_safe_filename(artist, title, "Karaoke
|
129
|
+
safe_filename = self._get_safe_filename(artist, title, "Karaoke", "toml")
|
129
130
|
toml_file = os.path.join(self.output_dir, safe_filename)
|
130
131
|
self.logger.debug(f"Generating TOML file: {toml_file}")
|
131
132
|
|
@@ -160,7 +161,7 @@ class CDGGenerator:
|
|
160
161
|
title=title,
|
161
162
|
artist=artist,
|
162
163
|
audio_file=audio_file,
|
163
|
-
output_name=f"{artist} - {title} (Karaoke
|
164
|
+
output_name=f"{artist} - {title} (Karaoke)",
|
164
165
|
sync_times=sync_times,
|
165
166
|
instrumentals=instrumentals,
|
166
167
|
formatted_lyrics=formatted_lyrics,
|
@@ -189,11 +190,11 @@ class CDGGenerator:
|
|
189
190
|
"""Compose CDG using KaraokeComposer."""
|
190
191
|
kc = KaraokeComposer.from_file(toml_file)
|
191
192
|
kc.compose()
|
192
|
-
kc.create_mp4(height=1080, fps=30)
|
193
|
+
# kc.create_mp4(height=1080, fps=30)
|
193
194
|
|
194
195
|
def _find_cdg_zip(self, artist: str, title: str) -> str:
|
195
196
|
"""Find the generated CDG ZIP file."""
|
196
|
-
safe_filename = self._get_safe_filename(artist, title, "Karaoke
|
197
|
+
safe_filename = self._get_safe_filename(artist, title, "Karaoke", "zip")
|
197
198
|
output_zip = os.path.join(self.output_dir, safe_filename)
|
198
199
|
|
199
200
|
self.logger.info(f"Looking for CDG ZIP file in output directory: {output_zip}")
|
@@ -215,12 +216,12 @@ class CDGGenerator:
|
|
215
216
|
|
216
217
|
def _get_cdg_path(self, artist: str, title: str) -> str:
|
217
218
|
"""Get the path to the CDG file."""
|
218
|
-
safe_filename = self._get_safe_filename(artist, title, "Karaoke
|
219
|
+
safe_filename = self._get_safe_filename(artist, title, "Karaoke", "cdg")
|
219
220
|
return os.path.join(self.output_dir, safe_filename)
|
220
221
|
|
221
222
|
def _get_mp3_path(self, artist: str, title: str) -> str:
|
222
223
|
"""Get the path to the MP3 file."""
|
223
|
-
safe_filename = self._get_safe_filename(artist, title, "Karaoke
|
224
|
+
safe_filename = self._get_safe_filename(artist, title, "Karaoke", "mp3")
|
224
225
|
return os.path.join(self.output_dir, safe_filename)
|
225
226
|
|
226
227
|
def _verify_output_files(self, cdg_file: str, mp3_file: str) -> None:
|
@@ -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,
|
@@ -375,7 +376,7 @@ class CDGGenerator:
|
|
375
376
|
cdg_styles: dict,
|
376
377
|
) -> dict:
|
377
378
|
"""Create TOML data structure."""
|
378
|
-
safe_output_name = self._get_safe_filename(artist, title, "Karaoke
|
379
|
+
safe_output_name = self._get_safe_filename(artist, title, "Karaoke")
|
379
380
|
return {
|
380
381
|
"title": title,
|
381
382
|
"artist": artist,
|
@@ -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
|
@@ -78,6 +78,7 @@ class OutputGenerator:
|
|
78
78
|
font_size=self.font_size,
|
79
79
|
line_height=self.line_height,
|
80
80
|
styles=self.config.styles,
|
81
|
+
subtitle_offset_ms=self.config.subtitle_offset_ms,
|
81
82
|
logger=self.logger,
|
82
83
|
)
|
83
84
|
|
@@ -161,12 +162,12 @@ class OutputGenerator:
|
|
161
162
|
"720p": (1280, 720),
|
162
163
|
"360p": (640, 360),
|
163
164
|
}
|
164
|
-
|
165
|
+
|
165
166
|
if resolution not in resolution_map:
|
166
167
|
raise ValueError("Invalid video_resolution value. Must be one of: 4k, 1080p, 720p, 360p")
|
167
|
-
|
168
|
+
|
168
169
|
resolution_dims = resolution_map[resolution]
|
169
|
-
|
170
|
+
|
170
171
|
# Default font sizes for each resolution
|
171
172
|
default_font_sizes = {
|
172
173
|
"4k": 250,
|
@@ -174,13 +175,13 @@ class OutputGenerator:
|
|
174
175
|
"720p": 100,
|
175
176
|
"360p": 40,
|
176
177
|
}
|
177
|
-
|
178
|
+
|
178
179
|
# Get font size from styles if available, otherwise use default
|
179
180
|
font_size = self.config.styles.get("karaoke", {}).get("font_size", default_font_sizes[resolution])
|
180
|
-
|
181
|
+
|
181
182
|
# Line height matches font size for all except 360p
|
182
183
|
line_height = 50 if resolution == "360p" else font_size
|
183
|
-
|
184
|
+
|
184
185
|
return resolution_dims, font_size, line_height
|
185
186
|
|
186
187
|
def write_corrections_data(self, correction_result: CorrectionResult, output_prefix: str) -> str:
|
@@ -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()
|
@@ -5,7 +5,7 @@ import subprocess
|
|
5
5
|
import json
|
6
6
|
|
7
7
|
from lyrics_transcriber.output.ass.section_screen import SectionScreen
|
8
|
-
from lyrics_transcriber.types import LyricsSegment
|
8
|
+
from lyrics_transcriber.types import LyricsSegment, Word
|
9
9
|
from lyrics_transcriber.output.ass import LyricsScreen, LyricsLine
|
10
10
|
from lyrics_transcriber.output.ass.ass import ASS
|
11
11
|
from lyrics_transcriber.output.ass.style import Style
|
@@ -25,6 +25,7 @@ class SubtitlesGenerator:
|
|
25
25
|
font_size: int,
|
26
26
|
line_height: int,
|
27
27
|
styles: dict,
|
28
|
+
subtitle_offset_ms: int = 0,
|
28
29
|
logger: Optional[logging.Logger] = None,
|
29
30
|
):
|
30
31
|
"""Initialize SubtitleGenerator.
|
@@ -34,12 +35,15 @@ class SubtitlesGenerator:
|
|
34
35
|
video_resolution: Tuple of (width, height) for video resolution
|
35
36
|
font_size: Font size for subtitles
|
36
37
|
line_height: Line height for subtitle positioning
|
38
|
+
styles: Dictionary of style configurations
|
39
|
+
subtitle_offset_ms: Offset for subtitle timing in milliseconds
|
37
40
|
logger: Optional logger instance
|
38
41
|
"""
|
39
42
|
self.output_dir = output_dir
|
40
43
|
self.video_resolution = video_resolution
|
41
44
|
self.font_size = font_size
|
42
45
|
self.styles = styles
|
46
|
+
self.subtitle_offset_ms = subtitle_offset_ms
|
43
47
|
self.config = ScreenConfig(line_height=line_height, video_width=video_resolution[0], video_height=video_resolution[1])
|
44
48
|
self.logger = logger or logging.getLogger(__name__)
|
45
49
|
|
@@ -91,6 +95,30 @@ class SubtitlesGenerator:
|
|
91
95
|
"""Create screens from segments with detailed logging."""
|
92
96
|
self.logger.debug("Creating screens from segments")
|
93
97
|
|
98
|
+
# Apply timing offset to segments if needed
|
99
|
+
if self.subtitle_offset_ms != 0:
|
100
|
+
self.logger.info(f"Subtitle offset: {self.subtitle_offset_ms}ms")
|
101
|
+
|
102
|
+
offset_seconds = self.subtitle_offset_ms / 1000.0
|
103
|
+
segments = [
|
104
|
+
LyricsSegment(
|
105
|
+
text=seg.text,
|
106
|
+
words=[
|
107
|
+
Word(
|
108
|
+
text=word.text,
|
109
|
+
start_time=max(0, word.start_time + offset_seconds),
|
110
|
+
end_time=word.end_time + offset_seconds,
|
111
|
+
confidence=word.confidence,
|
112
|
+
)
|
113
|
+
for word in seg.words
|
114
|
+
],
|
115
|
+
start_time=max(0, seg.start_time + offset_seconds),
|
116
|
+
end_time=seg.end_time + offset_seconds,
|
117
|
+
)
|
118
|
+
for seg in segments
|
119
|
+
]
|
120
|
+
self.logger.info(f"Applied {self.subtitle_offset_ms}ms offset to segment timings")
|
121
|
+
|
94
122
|
# Create section screens and get instrumental boundaries
|
95
123
|
section_screens = self._create_section_screens(segments, song_duration)
|
96
124
|
instrumental_times = self._get_instrumental_times(section_screens)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: lyrics-transcriber
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.41.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
|
@@ -1,8 +1,8 @@
|
|
1
1
|
lyrics_transcriber/__init__.py,sha256=JpdjDK1MH_Be2XiSQWnb4i5Bbil1uPMA_KcuDZ3cyUI,240
|
2
2
|
lyrics_transcriber/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
lyrics_transcriber/cli/cli_main.py,sha256=
|
3
|
+
lyrics_transcriber/cli/cli_main.py,sha256=MDgjIlgmKDfv3z6nK_j7TJKtcijfuLrB06hDMXXaZQY,10211
|
4
4
|
lyrics_transcriber/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
-
lyrics_transcriber/core/config.py,sha256=
|
5
|
+
lyrics_transcriber/core/config.py,sha256=euwOOtuNbXy4-a1xs8QKdjcf5jXZQle0zf6X1Wthurw,1229
|
6
6
|
lyrics_transcriber/core/controller.py,sha256=YKz-_nV-h2grXfQrNafDiK35uNitiLVA-z9BmMOkq-w,19540
|
7
7
|
lyrics_transcriber/correction/anchor_sequence.py,sha256=YpKyY24Va5i4JgzP9ssqlOIkaYu060KaldiehbfgTdk,22200
|
8
8
|
lyrics_transcriber/correction/corrector.py,sha256=r6AmdVl_JbOIbNgNk5aDHy-5vdw5-cayspUGht_844A,13494
|
@@ -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=Kx8_luGUQbrAlbzGKKNQTPTSGkN48oqOBIH6CYzNlMI,24894
|
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
|
@@ -111,11 +111,12 @@ lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf,sha256=WNG5LOQ-uGUF_WWT5aQ
|
|
111
111
|
lyrics_transcriber/output/fonts/arial.ttf,sha256=NcDzVZ2NtWnjbDEJW4pg1EFkPZX1kTneQOI_ragZuDM,275572
|
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
|
-
lyrics_transcriber/output/generator.py,sha256=
|
114
|
+
lyrics_transcriber/output/generator.py,sha256=FdXoyxp62jmkvOCaS1ixnRI9ek08ts1QeA8qxss_REY,8309
|
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
|
118
|
-
lyrics_transcriber/output/subtitles.py,sha256=
|
119
|
+
lyrics_transcriber/output/subtitles.py,sha256=jrvg0Tn8W-lRmlGNwnrMMzJB6xdEfI0ZoNLer5zWKJk,18170
|
119
120
|
lyrics_transcriber/output/video.py,sha256=ghb53OF6BNZy1VudKQvogPBA27eXfN8FHX9C72aGsm0,9095
|
120
121
|
lyrics_transcriber/review/__init__.py,sha256=_3Eqw-uXZhOZwo6_sHZLhP9vxAVkLF9EBXduUvPdLjQ,57
|
121
122
|
lyrics_transcriber/review/server.py,sha256=5Cfy3aS-h2c6bZU0wkrDZ9ATIkjndNWbxVJjQ89PFJE,5981
|
@@ -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.41.0.dist-info/LICENSE,sha256=BiPihPDxhxIPEx6yAxVfAljD5Bhm_XG2teCbPEj_m0Y,1069
|
130
|
+
lyrics_transcriber-0.41.0.dist-info/METADATA,sha256=civcml2WAL40CDoeC8zOI76uVoIGkGXe4oyvwWSEZGc,5891
|
131
|
+
lyrics_transcriber-0.41.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
132
|
+
lyrics_transcriber-0.41.0.dist-info/entry_points.txt,sha256=kcp-bSFkCACAEA0t166Kek0HpaJUXRo5SlF5tVrqNBU,216
|
133
|
+
lyrics_transcriber-0.41.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|