lyrics-transcriber 0.30.1__py3-none-any.whl → 0.32.2__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/cli_main.py +33 -12
- lyrics_transcriber/core/config.py +35 -0
- lyrics_transcriber/core/controller.py +85 -121
- lyrics_transcriber/correction/anchor_sequence.py +471 -0
- lyrics_transcriber/correction/corrector.py +237 -33
- 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 +5 -81
- lyrics_transcriber/lyrics/genius.py +5 -2
- lyrics_transcriber/lyrics/spotify.py +3 -3
- 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 +101 -193
- 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/transcribers/audioshake.py +3 -2
- lyrics_transcriber/transcribers/base_transcriber.py +5 -42
- lyrics_transcriber/transcribers/whisper.py +3 -4
- lyrics_transcriber/types.py +454 -0
- {lyrics_transcriber-0.30.1.dist-info → lyrics_transcriber-0.32.2.dist-info}/METADATA +14 -3
- lyrics_transcriber-0.32.2.dist-info/RECORD +86 -0
- {lyrics_transcriber-0.30.1.dist-info → lyrics_transcriber-0.32.2.dist-info}/WHEEL +1 -1
- {lyrics_transcriber-0.30.1.dist-info → lyrics_transcriber-0.32.2.dist-info}/entry_points.txt +1 -0
- lyrics_transcriber/correction/base_strategy.py +0 -29
- lyrics_transcriber/correction/strategy_diff.py +0 -263
- lyrics_transcriber-0.30.1.dist-info/RECORD +0 -25
- {lyrics_transcriber-0.30.1.dist-info → lyrics_transcriber-0.32.2.dist-info}/LICENSE +0 -0
@@ -1,305 +1,331 @@
|
|
1
|
-
|
2
|
-
from datetime import timedelta
|
3
|
-
from typing import Dict, List, Optional, Tuple
|
4
|
-
import json
|
5
|
-
import itertools
|
6
|
-
from pathlib import Path
|
7
|
-
from enum import IntEnum
|
1
|
+
import os
|
8
2
|
import logging
|
3
|
+
from typing import List, Optional, Tuple, Union
|
4
|
+
import subprocess
|
5
|
+
import json
|
9
6
|
|
10
|
-
from . import
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
def
|
50
|
-
"""
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
def
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
def set_segment_end_times(screens: List[LyricsScreen], song_duration_seconds: int) -> List[LyricsScreen]:
|
208
|
-
"""
|
209
|
-
Infer end times of lines for screens where they are not already set.
|
210
|
-
"""
|
211
|
-
segments = list(itertools.chain.from_iterable([l.segments for s in screens for l in s.lines]))
|
212
|
-
for i, segment in enumerate(segments):
|
213
|
-
if not segment.end_ts:
|
214
|
-
if i == len(segments) - 1:
|
215
|
-
segment.end_ts = timedelta(seconds=song_duration_seconds)
|
7
|
+
from lyrics_transcriber.output.ass.section_screen import SectionScreen
|
8
|
+
from lyrics_transcriber.types import LyricsSegment
|
9
|
+
from lyrics_transcriber.output.ass import LyricsScreen, LyricsLine
|
10
|
+
from lyrics_transcriber.output.ass.ass import ASS
|
11
|
+
from lyrics_transcriber.output.ass.style import Style
|
12
|
+
from lyrics_transcriber.output.ass.constants import ALIGN_TOP_CENTER
|
13
|
+
from lyrics_transcriber.output.ass import LyricsScreen
|
14
|
+
from lyrics_transcriber.output.ass.section_detector import SectionDetector
|
15
|
+
from lyrics_transcriber.output.ass.config import ScreenConfig
|
16
|
+
|
17
|
+
|
18
|
+
class SubtitlesGenerator:
|
19
|
+
"""Handles generation of subtitle files in various formats."""
|
20
|
+
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
output_dir: str,
|
24
|
+
video_resolution: Tuple[int, int],
|
25
|
+
font_size: int,
|
26
|
+
line_height: int,
|
27
|
+
styles: dict,
|
28
|
+
logger: Optional[logging.Logger] = None,
|
29
|
+
):
|
30
|
+
"""Initialize SubtitleGenerator.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
output_dir: Directory where output files will be written
|
34
|
+
video_resolution: Tuple of (width, height) for video resolution
|
35
|
+
font_size: Font size for subtitles
|
36
|
+
line_height: Line height for subtitle positioning
|
37
|
+
logger: Optional logger instance
|
38
|
+
"""
|
39
|
+
self.output_dir = output_dir
|
40
|
+
self.video_resolution = video_resolution
|
41
|
+
self.font_size = font_size
|
42
|
+
self.styles = styles
|
43
|
+
self.config = ScreenConfig(line_height=line_height, video_width=video_resolution[0], video_height=video_resolution[1])
|
44
|
+
self.logger = logger or logging.getLogger(__name__)
|
45
|
+
|
46
|
+
def _get_output_path(self, output_prefix: str, extension: str) -> str:
|
47
|
+
"""Generate full output path for a file."""
|
48
|
+
return os.path.join(self.output_dir, f"{output_prefix}.{extension}")
|
49
|
+
|
50
|
+
def _get_audio_duration(self, audio_filepath: str, segments: Optional[List[LyricsSegment]] = None) -> float:
|
51
|
+
"""Get audio duration using ffprobe."""
|
52
|
+
try:
|
53
|
+
probe_cmd = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "json", audio_filepath]
|
54
|
+
probe_output = subprocess.check_output(probe_cmd, universal_newlines=True)
|
55
|
+
probe_data = json.loads(probe_output)
|
56
|
+
duration = float(probe_data["format"]["duration"])
|
57
|
+
self.logger.debug(f"Detected audio duration: {duration:.2f}s")
|
58
|
+
return duration
|
59
|
+
except (subprocess.CalledProcessError, json.JSONDecodeError, KeyError) as e:
|
60
|
+
self.logger.error(f"Failed to get audio duration: {e}")
|
61
|
+
# Fallback to last segment end time plus buffer
|
62
|
+
if segments:
|
63
|
+
duration = segments[-1].end_time + 30.0
|
64
|
+
self.logger.warning(f"Using fallback duration: {duration:.2f}s")
|
65
|
+
return duration
|
66
|
+
return 0.0
|
67
|
+
|
68
|
+
def generate_ass(self, segments: List[LyricsSegment], output_prefix: str, audio_filepath: str) -> str:
|
69
|
+
self.logger.info("Generating ASS format subtitles")
|
70
|
+
output_path = self._get_output_path(f"{output_prefix} (Karaoke)", "ass")
|
71
|
+
|
72
|
+
try:
|
73
|
+
self.logger.debug(f"Processing {len(segments)} segments")
|
74
|
+
song_duration = self._get_audio_duration(audio_filepath, segments)
|
75
|
+
|
76
|
+
screens = self._create_screens(segments, song_duration)
|
77
|
+
self.logger.debug(f"Created {len(screens)} initial screens")
|
78
|
+
|
79
|
+
lyric_subtitles_ass = self._create_styled_subtitles(screens, self.video_resolution, self.font_size)
|
80
|
+
self.logger.debug("Created styled subtitles")
|
81
|
+
|
82
|
+
lyric_subtitles_ass.write(output_path)
|
83
|
+
self.logger.info(f"ASS file generated: {output_path}")
|
84
|
+
return output_path
|
85
|
+
|
86
|
+
except Exception as e:
|
87
|
+
self.logger.error(f"Failed to generate ASS file: {str(e)}", exc_info=True)
|
88
|
+
raise
|
89
|
+
|
90
|
+
def _create_screens(self, segments: List[LyricsSegment], song_duration: float) -> List[LyricsScreen]:
|
91
|
+
"""Create screens from segments with detailed logging."""
|
92
|
+
self.logger.debug("Creating screens from segments")
|
93
|
+
|
94
|
+
# Create section screens and get instrumental boundaries
|
95
|
+
section_screens = self._create_section_screens(segments, song_duration)
|
96
|
+
instrumental_times = self._get_instrumental_times(section_screens)
|
97
|
+
|
98
|
+
# Create regular lyric screens
|
99
|
+
lyric_screens = self._create_lyric_screens(segments, instrumental_times)
|
100
|
+
|
101
|
+
# Merge and process all screens
|
102
|
+
all_screens = self._merge_and_process_screens(section_screens, lyric_screens)
|
103
|
+
|
104
|
+
# Log final results
|
105
|
+
self._log_final_screens(all_screens)
|
106
|
+
|
107
|
+
return all_screens
|
108
|
+
|
109
|
+
def _create_section_screens(self, segments: List[LyricsSegment], song_duration: float) -> List[SectionScreen]:
|
110
|
+
"""Create section screens using SectionDetector."""
|
111
|
+
section_detector = SectionDetector(logger=self.logger)
|
112
|
+
return section_detector.process_segments(segments, self.video_resolution, self.config.line_height, song_duration)
|
113
|
+
|
114
|
+
def _get_instrumental_times(self, section_screens: List[SectionScreen]) -> List[Tuple[float, float]]:
|
115
|
+
"""Extract instrumental section time boundaries."""
|
116
|
+
instrumental_times = [
|
117
|
+
(s.start_time, s.end_time) for s in section_screens if isinstance(s, SectionScreen) and s.section_type == "INSTRUMENTAL"
|
118
|
+
]
|
119
|
+
|
120
|
+
self.logger.debug(f"Found {len(instrumental_times)} instrumental sections:")
|
121
|
+
for start, end in instrumental_times:
|
122
|
+
self.logger.debug(f" {start:.2f}s - {end:.2f}s")
|
123
|
+
|
124
|
+
return instrumental_times
|
125
|
+
|
126
|
+
def _create_lyric_screens(self, segments: List[LyricsSegment], instrumental_times: List[Tuple[float, float]]) -> List[LyricsScreen]:
|
127
|
+
"""Create regular lyric screens, handling instrumental boundaries."""
|
128
|
+
screens: List[LyricsScreen] = []
|
129
|
+
current_screen: Optional[LyricsScreen] = None
|
130
|
+
|
131
|
+
for i, segment in enumerate(segments):
|
132
|
+
self.logger.debug(f"Processing segment {i}: {segment.start_time:.2f}s - {segment.end_time:.2f}s")
|
133
|
+
|
134
|
+
# Skip segments in instrumental sections
|
135
|
+
if self._is_in_instrumental_section(segment, instrumental_times):
|
136
|
+
continue
|
137
|
+
|
138
|
+
# Check if we need a new screen
|
139
|
+
if self._should_start_new_screen(current_screen, segment, instrumental_times):
|
140
|
+
# fmt: off
|
141
|
+
current_screen = LyricsScreen(
|
142
|
+
video_size=self.video_resolution,
|
143
|
+
line_height=self.config.line_height,
|
144
|
+
config=self.config,
|
145
|
+
logger=self.logger
|
146
|
+
)
|
147
|
+
# fmt: on
|
148
|
+
screens.append(current_screen)
|
149
|
+
self.logger.debug(" Created new screen")
|
150
|
+
|
151
|
+
# Add line to current screen
|
152
|
+
line = LyricsLine(logger=self.logger, segment=segment, screen_config=self.config)
|
153
|
+
current_screen.lines.append(line)
|
154
|
+
self.logger.debug(f" Added line to screen (now has {len(current_screen.lines)} lines)")
|
155
|
+
|
156
|
+
return screens
|
157
|
+
|
158
|
+
def _is_in_instrumental_section(self, segment: LyricsSegment, instrumental_times: List[Tuple[float, float]]) -> bool:
|
159
|
+
"""Check if a segment falls within any instrumental section."""
|
160
|
+
for inst_start, inst_end in instrumental_times:
|
161
|
+
if segment.start_time >= inst_start and segment.start_time < inst_end:
|
162
|
+
self.logger.debug(f" Skipping segment - falls within instrumental {inst_start:.2f}s - {inst_end:.2f}s")
|
163
|
+
return True
|
164
|
+
return False
|
165
|
+
|
166
|
+
def _should_start_new_screen(
|
167
|
+
self, current_screen: Optional[LyricsScreen], segment: LyricsSegment, instrumental_times: List[Tuple[float, float]]
|
168
|
+
) -> bool:
|
169
|
+
"""Determine if a new screen should be started."""
|
170
|
+
if current_screen is None:
|
171
|
+
return True
|
172
|
+
|
173
|
+
if len(current_screen.lines) >= self.config.max_visible_lines:
|
174
|
+
return True
|
175
|
+
|
176
|
+
# Check if this segment is first after any instrumental section
|
177
|
+
if current_screen.lines:
|
178
|
+
prev_segment = current_screen.lines[-1].segment
|
179
|
+
for inst_start, inst_end in instrumental_times:
|
180
|
+
if prev_segment.end_time <= inst_start and segment.start_time >= inst_end:
|
181
|
+
self.logger.debug(f" Forcing new screen - first segment after instrumental {inst_start:.2f}s - {inst_end:.2f}s")
|
182
|
+
return True
|
183
|
+
|
184
|
+
return False
|
185
|
+
|
186
|
+
def _merge_and_process_screens(
|
187
|
+
self, section_screens: List[SectionScreen], lyric_screens: List[LyricsScreen]
|
188
|
+
) -> List[Union[SectionScreen, LyricsScreen]]:
|
189
|
+
"""Merge section and lyric screens in chronological order."""
|
190
|
+
# Sort all screens by start time
|
191
|
+
return sorted(section_screens + lyric_screens, key=lambda s: s.start_ts)
|
192
|
+
|
193
|
+
def _log_final_screens(self, screens: List[Union[SectionScreen, LyricsScreen]]) -> None:
|
194
|
+
"""Log details of all final screens."""
|
195
|
+
self.logger.debug("Final screens created:")
|
196
|
+
for i, screen in enumerate(screens):
|
197
|
+
self.logger.debug(f"Screen {i + 1}:")
|
198
|
+
if isinstance(screen, SectionScreen):
|
199
|
+
self.logger.debug(f" Section: {screen.section_type}")
|
200
|
+
self.logger.debug(f" Text: {screen.text}")
|
201
|
+
self.logger.debug(f" Time: {screen.start_time:.2f}s - {screen.end_time:.2f}s")
|
216
202
|
else:
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
"
|
254
|
-
"
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
"
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
203
|
+
self.logger.debug(f" Number of lines: {len(screen.lines)}")
|
204
|
+
for j, line in enumerate(screen.lines):
|
205
|
+
self.logger.debug(f" Line {j + 1} ({line.segment.start_time:.2f}s - {line.segment.end_time:.2f}s): {line}")
|
206
|
+
|
207
|
+
def _create_styled_ass_instance(self, resolution, fontsize):
|
208
|
+
a = ASS()
|
209
|
+
a.set_resolution(resolution)
|
210
|
+
|
211
|
+
a.styles_format = [
|
212
|
+
"Name", # The name of the Style. Case sensitive. Cannot include commas.
|
213
|
+
"Fontname", # The fontname as used by Windows. Case-sensitive.
|
214
|
+
"Fontpath", # The path to the font file.
|
215
|
+
"Fontsize", # Font size
|
216
|
+
"PrimaryColour", # This is the colour that a subtitle will normally appear in.
|
217
|
+
"SecondaryColour", # This colour may be used instead of the Primary colour when a subtitle is automatically shifted to prevent an onscreen collsion, to distinguish the different subtitles.
|
218
|
+
"OutlineColour", # This colour may be used instead of the Primary or Secondary colour when a subtitle is automatically shifted to prevent an onscreen collsion, to distinguish the different subtitles.
|
219
|
+
"BackColour", # This is the colour of the subtitle outline or shadow, if these are used
|
220
|
+
"Bold", # This defines whether text is bold (true) or not (false). -1 is True, 0 is False
|
221
|
+
"Italic", # This defines whether text is italic (true) or not (false). -1 is True, 0 is False
|
222
|
+
"Underline", # [-1 or 0]
|
223
|
+
"StrikeOut", # [-1 or 0]
|
224
|
+
"ScaleX", # Modifies the width of the font. [percent]
|
225
|
+
"ScaleY", # Modifies the height of the font. [percent]
|
226
|
+
"Spacing", # Extra space between characters. [pixels]
|
227
|
+
"Angle", # The origin of the rotation is defined by the alignment. Can be a floating point number. [degrees]
|
228
|
+
"BorderStyle", # 1=Outline + drop shadow, 3=Opaque box
|
229
|
+
"Outline", # If BorderStyle is 1, then this specifies the width of the outline around the text, in pixels. Values may be 0, 1, 2, 3 or 4.
|
230
|
+
"Shadow", # If BorderStyle is 1, then this specifies the depth of the drop shadow behind the text, in pixels. Values may be 0, 1, 2, 3 or 4. Drop shadow is always used in addition to an outline - SSA will force an outline of 1 pixel if no outline width is given.
|
231
|
+
"Alignment", # This sets how text is "justified" within the Left/Right onscreen margins, and also the vertical placing. Values may be 1=Left, 2=Centered, 3=Right. Add 4 to the value for a "Toptitle". Add 8 to the value for a "Midtitle". eg. 5 = left-justified toptitle
|
232
|
+
"MarginL", # This defines the Left Margin in pixels. It is the distance from the left-hand edge of the screen.The three onscreen margins (MarginL, MarginR, MarginV) define areas in which the subtitle text will be displayed.
|
233
|
+
"MarginR", # This defines the Right Margin in pixels. It is the distance from the right-hand edge of the screen.
|
234
|
+
"MarginV", # MarginV. This defines the vertical Left Margin in pixels. For a subtitle, it is the distance from the bottom of the screen. For a toptitle, it is the distance from the top of the screen. For a midtitle, the value is ignored - the text will be vertically centred
|
235
|
+
"Encoding", #
|
236
|
+
]
|
237
|
+
|
238
|
+
# Get font settings from styles
|
239
|
+
karaoke_styles = self.styles.get("karaoke", {})
|
240
|
+
font_path = karaoke_styles.get("font_path")
|
241
|
+
|
242
|
+
style = Style()
|
243
|
+
|
244
|
+
style.type = "Style"
|
245
|
+
style.Name = self.styles["karaoke"]["ass_name"]
|
246
|
+
style.Fontname = self.styles["karaoke"]["font"]
|
247
|
+
style.Fontpath = font_path
|
248
|
+
style.Fontsize = fontsize
|
249
|
+
|
250
|
+
style.Alignment = ALIGN_TOP_CENTER
|
251
|
+
|
252
|
+
# Convert color strings to tuples of integers
|
253
|
+
def parse_color(color_str):
|
254
|
+
return tuple(int(x.strip()) for x in color_str.split(","))
|
255
|
+
|
256
|
+
style.PrimaryColour = parse_color(self.styles["karaoke"]["primary_color"])
|
257
|
+
style.SecondaryColour = parse_color(self.styles["karaoke"]["secondary_color"])
|
258
|
+
style.OutlineColour = parse_color(self.styles["karaoke"]["outline_color"])
|
259
|
+
style.BackColour = parse_color(self.styles["karaoke"]["back_color"])
|
260
|
+
|
261
|
+
# Convert boolean strings to integers (-1 for True, 0 for False)
|
262
|
+
def parse_bool(value):
|
263
|
+
return -1 if value else 0
|
264
|
+
|
265
|
+
style.Bold = parse_bool(self.styles["karaoke"]["bold"])
|
266
|
+
style.Italic = parse_bool(self.styles["karaoke"]["italic"])
|
267
|
+
style.Underline = parse_bool(self.styles["karaoke"]["underline"])
|
268
|
+
style.StrikeOut = parse_bool(self.styles["karaoke"]["strike_out"])
|
269
|
+
|
270
|
+
# Convert numeric strings to appropriate types
|
271
|
+
style.ScaleX = int(self.styles["karaoke"]["scale_x"])
|
272
|
+
style.ScaleY = int(self.styles["karaoke"]["scale_y"])
|
273
|
+
style.Spacing = int(self.styles["karaoke"]["spacing"])
|
274
|
+
style.Angle = float(self.styles["karaoke"]["angle"])
|
275
|
+
style.BorderStyle = int(self.styles["karaoke"]["border_style"])
|
276
|
+
style.Outline = int(self.styles["karaoke"]["outline"])
|
277
|
+
style.Shadow = int(self.styles["karaoke"]["shadow"])
|
278
|
+
style.MarginL = int(self.styles["karaoke"]["margin_l"])
|
279
|
+
style.MarginR = int(self.styles["karaoke"]["margin_r"])
|
280
|
+
style.MarginV = int(self.styles["karaoke"]["margin_v"])
|
281
|
+
style.Encoding = int(self.styles["karaoke"]["encoding"])
|
282
|
+
|
283
|
+
a.add_style(style)
|
284
|
+
|
285
|
+
a.events_format = ["Layer", "Style", "Start", "End", "MarginV", "Text"]
|
286
|
+
return a, style
|
287
|
+
|
288
|
+
def _create_styled_subtitles(
|
289
|
+
self,
|
290
|
+
screens: List[Union[SectionScreen, LyricsScreen]],
|
291
|
+
resolution: Tuple[int, int],
|
292
|
+
fontsize: int,
|
293
|
+
) -> ASS:
|
294
|
+
"""Create styled ASS subtitles from all screens."""
|
295
|
+
ass_file, style = self._create_styled_ass_instance(resolution, fontsize)
|
296
|
+
|
297
|
+
active_lines = []
|
298
|
+
previous_instrumental_end = None
|
299
|
+
|
300
|
+
for screen in screens:
|
301
|
+
if isinstance(screen, SectionScreen):
|
302
|
+
# Create section marker events (returns tuple of ([event], []))
|
303
|
+
section_events, _ = screen.as_ass_events(style=style)
|
304
|
+
for event in section_events: # Now we're iterating over the list of events
|
305
|
+
ass_file.add(event)
|
306
|
+
|
307
|
+
previous_instrumental_end = screen.end_time
|
308
|
+
active_lines = []
|
309
|
+
self.logger.debug(f"Found instrumental section ending at {screen.end_time:.2f}s")
|
310
|
+
continue
|
311
|
+
|
312
|
+
# Process screen and get its events
|
313
|
+
self.logger.debug(f"Processing screen with instrumental_end={previous_instrumental_end}")
|
314
|
+
# fmt: off
|
315
|
+
events, active_lines = screen.as_ass_events(
|
316
|
+
style=style,
|
317
|
+
previous_active_lines=active_lines,
|
318
|
+
previous_instrumental_end=previous_instrumental_end
|
319
|
+
)
|
320
|
+
# fmt: on
|
321
|
+
|
322
|
+
# Only reset instrumental end after we've processed the first post-instrumental screen
|
323
|
+
if previous_instrumental_end is not None:
|
324
|
+
self.logger.debug("Clearing instrumental end time after processing post-instrumental screen")
|
325
|
+
previous_instrumental_end = None
|
326
|
+
|
327
|
+
# Add all events to ASS file
|
328
|
+
for event in events:
|
329
|
+
ass_file.add(event)
|
330
|
+
|
331
|
+
return ass_file
|