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,252 @@
1
+ from dataclasses import dataclass
2
+ from typing import List, Optional, Tuple
3
+ import logging
4
+ from datetime import timedelta
5
+
6
+ from lyrics_transcriber.output.ass.style import Style
7
+ from lyrics_transcriber.output.ass.event import Event
8
+ from lyrics_transcriber.output.ass.lyrics_line import LyricsLine
9
+ from lyrics_transcriber.output.ass.config import ScreenConfig, LineTimingInfo, LineState
10
+
11
+
12
+ class PositionStrategy:
13
+ """Handles calculation of vertical positions for lines."""
14
+
15
+ def __init__(self, video_size: Tuple[int, int], config: ScreenConfig):
16
+ self.video_size = video_size
17
+ self.config = config
18
+
19
+ def calculate_line_positions(self) -> List[int]:
20
+ """Calculate vertical positions for all possible lines."""
21
+ return PositionCalculator.calculate_line_positions(self.config)
22
+
23
+
24
+ class PositionCalculator:
25
+ """Shared position calculation logic."""
26
+
27
+ @staticmethod
28
+ def calculate_first_line_position(config: ScreenConfig) -> int:
29
+ """Calculate vertical position of first line."""
30
+ total_height = config.max_visible_lines * config.line_height
31
+ return config.top_padding + (config.video_height - total_height - config.top_padding) // 4
32
+
33
+ @staticmethod
34
+ def calculate_line_positions(config: ScreenConfig) -> List[int]:
35
+ """Calculate vertical positions for all possible lines."""
36
+ first_pos = PositionCalculator.calculate_first_line_position(config)
37
+ return [first_pos + (i * config.line_height) for i in range(config.max_visible_lines)]
38
+
39
+ @staticmethod
40
+ def position_to_line_index(y_position: int, config: ScreenConfig) -> int:
41
+ """Convert y-position to 0-based line index."""
42
+ first_pos = PositionCalculator.calculate_first_line_position(config)
43
+ return (y_position - first_pos) // config.line_height
44
+
45
+ @staticmethod
46
+ def line_index_to_position(index: int, config: ScreenConfig) -> int:
47
+ """Convert 0-based line index to y-position."""
48
+ first_pos = PositionCalculator.calculate_first_line_position(config)
49
+ return first_pos + (index * config.line_height)
50
+
51
+
52
+ class TimingStrategy:
53
+ """Handles calculation of line timings during screen transitions."""
54
+
55
+ def __init__(self, config: ScreenConfig, logger: Optional[logging.Logger] = None):
56
+ self.config = config
57
+ self.logger = logger or logging.getLogger(__name__)
58
+
59
+ def calculate_line_timings(
60
+ self,
61
+ current_lines: List[LyricsLine],
62
+ previous_active_lines: Optional[List[Tuple[float, int, str]]] = None,
63
+ previous_instrumental_end: Optional[float] = None,
64
+ ) -> List[LineTimingInfo]:
65
+ """Calculate timing information for each line in the screen."""
66
+ previous_active_lines = previous_active_lines or []
67
+
68
+ # If no previous lines, determine appropriate start time
69
+ if not previous_active_lines:
70
+ if previous_instrumental_end is not None:
71
+ # For post-instrumental, start right after the instrumental
72
+ return self._calculate_simultaneous_timings(current_lines, fade_in_start=previous_instrumental_end)
73
+ else:
74
+ # For first screen, start at 0
75
+ return self._calculate_simultaneous_timings(current_lines, fade_in_start=0.0)
76
+
77
+ # Create a map of position -> clear time from previous lines
78
+ position_clear_times = {}
79
+ for end_time, y_position, text in previous_active_lines:
80
+ # Clear time is now just when the fade out ends
81
+ clear_time = end_time + (self.config.fade_out_ms / 1000)
82
+ position_clear_times[y_position] = clear_time
83
+
84
+ # Add buffer time for the position above the active line
85
+ # Use the end time without fade for the line above to give more reading time
86
+ line_index = PositionCalculator.position_to_line_index(y_position, self.config)
87
+ # fmt: off
88
+ if line_index > 0: # If not the top line
89
+ position_above = PositionCalculator.line_index_to_position(line_index - 1, self.config)
90
+ position_clear_times[position_above] = max(
91
+ position_clear_times.get(position_above, 0),
92
+ end_time # Use end time without fade for position above
93
+ )
94
+ # fmt: on
95
+
96
+ self.logger.debug(f" Position {line_index + 1} occupied by '{text}' until {clear_time:.2f}s")
97
+
98
+ # Calculate timings for each line
99
+ timings = []
100
+ positions = PositionCalculator.calculate_line_positions(self.config)
101
+ for i, (line, position) in enumerate(zip(current_lines, positions)):
102
+ # Fade in as soon as the position is available
103
+ fade_in_time = position_clear_times.get(position, 0)
104
+
105
+ # Calculate remaining timing information
106
+ end_time = line.segment.end_time + self.config.post_roll_time
107
+ fade_out_time = end_time + (self.config.fade_out_ms / 1000)
108
+ # Clear time is now just the fade out time
109
+ clear_time = fade_out_time
110
+
111
+ # fmt: off
112
+ timing = LineTimingInfo(
113
+ fade_in_time=fade_in_time,
114
+ end_time=end_time,
115
+ fade_out_time=fade_out_time,
116
+ clear_time=clear_time
117
+ )
118
+ # fmt: on
119
+ timings.append(timing)
120
+
121
+ line_index = PositionCalculator.position_to_line_index(position, self.config)
122
+ self.logger.debug(
123
+ f" Line {line_index + 1}: '{line.segment.text}' "
124
+ f"fades in at {fade_in_time:.2f}s "
125
+ f"(position available at {position_clear_times.get(position, 0):.2f}s)"
126
+ )
127
+
128
+ return timings
129
+
130
+ def _calculate_simultaneous_timings(self, lines: List[LyricsLine], fade_in_start: float) -> List[LineTimingInfo]:
131
+ """Calculate timings for screens where all lines appear together."""
132
+ return [
133
+ LineTimingInfo(
134
+ fade_in_time=fade_in_start,
135
+ end_time=line.segment.end_time + self.config.post_roll_time,
136
+ fade_out_time=line.segment.end_time + self.config.post_roll_time + (self.config.fade_out_ms / 1000),
137
+ clear_time=line.segment.end_time + self.config.post_roll_time + (self.config.fade_out_ms / 1000),
138
+ )
139
+ for line in lines
140
+ ]
141
+
142
+
143
+ @dataclass
144
+ class LyricsScreen:
145
+ """Represents a screen of lyrics (multiple lines)."""
146
+
147
+ video_size: Tuple[int, int]
148
+ line_height: int
149
+ lines: List[LyricsLine] = None # Make lines optional
150
+ logger: Optional[logging.Logger] = None
151
+ post_instrumental: bool = False
152
+ config: Optional[ScreenConfig] = None
153
+
154
+ def __post_init__(self):
155
+ if self.logger is None:
156
+ self.logger = logging.getLogger(__name__)
157
+ if self.config is None:
158
+ self.config = ScreenConfig(line_height=self.line_height)
159
+ else:
160
+ # Ensure line_height is consistent
161
+ self.config.line_height = self.line_height
162
+
163
+ # Initialize empty lines list if None
164
+ if self.lines is None:
165
+ self.lines = []
166
+
167
+ # Update video height in config
168
+ self.config.video_width = self.video_size[0]
169
+ self.config.video_height = self.video_size[1]
170
+
171
+ # Initialize strategies
172
+ self.position_strategy = PositionStrategy(self.video_size, self.config)
173
+ self.timing_strategy = TimingStrategy(self.config, self.logger)
174
+
175
+ def as_ass_events(
176
+ self,
177
+ style: Style,
178
+ previous_active_lines: Optional[List[Tuple[float, int, str]]] = None,
179
+ previous_instrumental_end: Optional[float] = None,
180
+ ) -> Tuple[List[Event], List[Tuple[float, int, str]]]:
181
+ """Convert screen to ASS events. Returns (events, active_lines)."""
182
+ events = []
183
+ active_lines = []
184
+ previous_active_lines = previous_active_lines or []
185
+
186
+ # Find the latest end time from previous lines
187
+ previous_end_time = None
188
+ if previous_active_lines:
189
+ previous_end_time = max(end_time for end_time, _, _ in previous_active_lines)
190
+
191
+ # Log active lines from previous screen
192
+ if previous_active_lines:
193
+ self.logger.debug(" Active lines from previous screen:")
194
+ for end, pos, text in previous_active_lines:
195
+ # Convert y-position back to line index (0-based)
196
+ line_index = PositionCalculator.position_to_line_index(pos, self.config)
197
+ clear_time = end + (self.config.fade_out_ms / 1000)
198
+ self.logger.debug(
199
+ f" Line {line_index + 1}: '{text}' "
200
+ f"(ends {end:.2f}s, fade out {end + (self.config.fade_out_ms / 1000):.2f}s, clear {clear_time:.2f}s)"
201
+ )
202
+
203
+ # Calculate positions and timings
204
+ positions = self.position_strategy.calculate_line_positions()
205
+ # fmt: off
206
+ timings = self.timing_strategy.calculate_line_timings(
207
+ current_lines=self.lines,
208
+ previous_active_lines=previous_active_lines,
209
+ previous_instrumental_end=previous_instrumental_end
210
+ )
211
+ # fmt: on
212
+
213
+ # Create line states and events
214
+ for i, (line, timing) in enumerate(zip(self.lines, timings)):
215
+ y_position = positions[i]
216
+
217
+ # Create line state
218
+ line_state = LineState(text=line.segment.text, timing=timing, y_position=y_position)
219
+
220
+ # Create ASS events with previous end time info
221
+ # fmt: off
222
+ line_events = line.create_ass_events(
223
+ state=line_state,
224
+ style=style,
225
+ config=self.config,
226
+ previous_end_time=previous_end_time
227
+ )
228
+ # fmt: on
229
+ events.extend(line_events)
230
+
231
+ # Track this line's end time for the next screen
232
+ previous_end_time = timing.end_time
233
+ active_lines.append((timing.end_time, y_position, line.segment.text))
234
+
235
+ # Log line placement with index
236
+ self.logger.debug(f" Line {i + 1}: '{line.segment.text}'")
237
+
238
+ return events, active_lines
239
+
240
+ @property
241
+ def start_ts(self) -> timedelta:
242
+ """Get screen start timestamp."""
243
+ return timedelta(seconds=min(line.segment.start_time for line in self.lines))
244
+
245
+ @property
246
+ def end_ts(self) -> timedelta:
247
+ """Get screen end timestamp."""
248
+ latest_ts = max(line.segment.end_time for line in self.lines)
249
+ return timedelta(seconds=latest_ts + self.config.post_roll_time)
250
+
251
+ def __str__(self):
252
+ return "\n".join([f"{self.start_ts} - {self.end_ts}:", *[f"\t{line}" for line in self.lines]])
@@ -0,0 +1,89 @@
1
+ from typing import List, Optional, Tuple
2
+ import logging
3
+
4
+ from lyrics_transcriber.types import LyricsSegment
5
+ from lyrics_transcriber.output.ass import LyricsScreen, SectionScreen
6
+
7
+
8
+ class SectionDetector:
9
+ """Detects and creates section screens between lyrics."""
10
+
11
+ def __init__(self, gap_threshold: float = 10.0, logger: Optional[logging.Logger] = None):
12
+ self.gap_threshold = gap_threshold
13
+ self.logger = logger or logging.getLogger(__name__)
14
+ self.intro_padding = 0.0 # No padding for intro
15
+ self.outro_padding = 5.0 # End 5s before song ends
16
+ self.instrumental_start_padding = 1.0 # Start 1s after previous segment
17
+ self.instrumental_end_padding = 5.0 # End 5s before next segment
18
+
19
+ def process_segments(
20
+ self, segments: List[LyricsSegment], video_size: Tuple[int, int], line_height: int, song_duration: float
21
+ ) -> List[LyricsScreen]:
22
+ """Process segments and insert section screens where appropriate.
23
+
24
+ Args:
25
+ segments: List of lyric segments
26
+ video_size: Tuple of (width, height) for video resolution
27
+ line_height: Height of each line in pixels
28
+ song_duration: Total duration of the song in seconds
29
+ """
30
+ if not segments:
31
+ return []
32
+
33
+ screens: List[LyricsScreen] = []
34
+
35
+ # Check for intro
36
+ if segments[0].start_time >= self.gap_threshold:
37
+ self.logger.debug(f"Detected intro section: 0.0 - {segments[0].start_time:.2f}s")
38
+ screens.append(
39
+ SectionScreen(
40
+ section_type="INTRO",
41
+ start_time=0.0,
42
+ end_time=segments[0].start_time - self.intro_padding,
43
+ video_size=video_size,
44
+ line_height=line_height,
45
+ logger=self.logger,
46
+ )
47
+ )
48
+
49
+ # Check for instrumental sections between segments
50
+ for i in range(len(segments) - 1):
51
+ gap = segments[i + 1].start_time - segments[i].end_time
52
+ if gap >= self.gap_threshold:
53
+ instrumental_start = segments[i].end_time + self.instrumental_start_padding
54
+ instrumental_end = segments[i + 1].start_time - self.instrumental_end_padding
55
+
56
+ # Only create section if there's meaningful duration after padding
57
+ if instrumental_end > instrumental_start:
58
+ self.logger.debug(f"Detected instrumental section: {instrumental_start:.2f} - {instrumental_end:.2f}s")
59
+ screens.append(
60
+ SectionScreen(
61
+ section_type="INSTRUMENTAL",
62
+ start_time=instrumental_start,
63
+ end_time=instrumental_end,
64
+ video_size=video_size,
65
+ line_height=line_height,
66
+ logger=self.logger,
67
+ )
68
+ )
69
+
70
+ # Check for outro
71
+ if segments: # Only add outro if there are segments
72
+ last_segment = segments[-1]
73
+ outro_duration = song_duration - last_segment.end_time
74
+ if outro_duration >= self.gap_threshold:
75
+ outro_start = last_segment.end_time + self.instrumental_start_padding
76
+ outro_end = song_duration - self.outro_padding # End 5s before song ends
77
+ self.logger.debug(f"Detected outro section: {outro_start:.2f}s - {outro_end:.2f}s")
78
+ screens.append(
79
+ SectionScreen(
80
+ section_type="OUTRO",
81
+ start_time=outro_start,
82
+ end_time=outro_end,
83
+ video_size=video_size,
84
+ line_height=line_height,
85
+ logger=self.logger,
86
+ )
87
+ )
88
+
89
+ return screens
@@ -0,0 +1,106 @@
1
+ from dataclasses import dataclass
2
+ from typing import List, Optional, Literal, Tuple
3
+ import logging
4
+ from datetime import timedelta
5
+
6
+ from lyrics_transcriber.output.ass.style import Style
7
+ from lyrics_transcriber.output.ass.event import Event
8
+ from lyrics_transcriber.output.ass.lyrics_screen import ScreenConfig
9
+
10
+
11
+ @dataclass
12
+ class SectionScreen:
13
+ """Special screen for instrumental sections."""
14
+
15
+ section_type: Literal["INTRO", "OUTRO", "INSTRUMENTAL"]
16
+ start_time: float
17
+ end_time: float
18
+ video_size: Tuple[int, int]
19
+ line_height: int
20
+ logger: Optional[logging.Logger] = None
21
+ config: Optional[ScreenConfig] = None
22
+
23
+ def __post_init__(self):
24
+ self._initialize_logger_and_config()
25
+ self._calculate_duration()
26
+ self._adjust_timing()
27
+ self._create_text()
28
+
29
+ def _initialize_logger_and_config(self):
30
+ """Initialize logger and config with defaults if not provided."""
31
+ if self.logger is None:
32
+ self.logger = logging.getLogger(__name__)
33
+ if self.config is None:
34
+ self.config = ScreenConfig(line_height=self.line_height)
35
+
36
+ self.config.video_width = self.video_size[0]
37
+ self.config.video_height = self.video_size[1]
38
+
39
+ def _calculate_duration(self):
40
+ """Calculate duration before any timing adjustments."""
41
+ self.original_duration = round(self.end_time - self.start_time)
42
+
43
+ def _adjust_timing(self):
44
+ """Apply timing adjustments based on section type."""
45
+ if self.section_type == "INTRO":
46
+ self.start_time = 1.0 # Start after 1 second
47
+ self.end_time = self.end_time - 5.0 # End 5 seconds before next section
48
+
49
+ def _create_text(self):
50
+ """Create the section text with duration."""
51
+ self.text = f"♪ {self.section_type} ({self.original_duration} seconds) ♪"
52
+
53
+ def _calculate_start_time(self, previous_active_lines: Optional[List[Tuple[float, int, str]]] = None) -> float:
54
+ """Calculate start time accounting for previous lines."""
55
+ start_time = self.start_time
56
+ if previous_active_lines:
57
+ latest_end = max(end + (self.config.fade_out_ms / 1000) for end, _, _ in previous_active_lines)
58
+ start_time = max(start_time, latest_end)
59
+ return start_time
60
+
61
+ def _calculate_vertical_position(self) -> int:
62
+ """Calculate vertical position for centered text."""
63
+ return (self.video_size[1] - self.line_height) // 2
64
+
65
+ def _create_event(self, style: Style, start_time: float) -> Event:
66
+ """Create an ASS event with proper formatting."""
67
+ event = Event()
68
+ event.type = "Dialogue"
69
+ event.Layer = 0
70
+ event.Style = style
71
+ event.Start = start_time
72
+ event.End = self.end_time
73
+ event.MarginV = self._calculate_vertical_position()
74
+
75
+ # Add karaoke timing for the entire duration
76
+ duration = int((self.end_time - start_time) * 100) # Convert to centiseconds
77
+ event.Text = f"{{\\fad({self.config.fade_in_ms},{self.config.fade_out_ms})}}" f"{{\\an8}}{{\\K{duration}}}{self.text}"
78
+ return event
79
+
80
+ def as_ass_events(
81
+ self,
82
+ style: Style,
83
+ previous_active_lines: Optional[List[Tuple[float, int, str]]] = None,
84
+ previous_instrumental_end: Optional[float] = None,
85
+ ) -> Tuple[List[Event], List[Tuple[float, int, str]]]:
86
+ """Create ASS events for section markers with karaoke highlighting."""
87
+ self.logger.debug(f"Creating section marker event for {self.section_type}")
88
+
89
+ start_time = self._calculate_start_time(previous_active_lines)
90
+ event = self._create_event(style, start_time)
91
+
92
+ self.logger.debug(f"Created section event: {event.Text} ({event.Start}s - {event.End}s)")
93
+ return [event], [] # No active lines to track for sections
94
+
95
+ @property
96
+ def start_ts(self) -> timedelta:
97
+ """Get start timestamp."""
98
+ return timedelta(seconds=self.start_time)
99
+
100
+ @property
101
+ def end_ts(self) -> timedelta:
102
+ """Get end timestamp."""
103
+ return timedelta(seconds=self.end_time)
104
+
105
+ def __str__(self):
106
+ return f"{self.section_type} {self.start_ts} - {self.end_ts}"
@@ -0,0 +1,187 @@
1
+ from lyrics_transcriber.output.ass.formatters import Formatters
2
+ from lyrics_transcriber.output.ass.constants import ALIGN_BOTTOM_CENTER
3
+
4
+
5
+ class Style:
6
+ aliases = {}
7
+ formatters = None
8
+ order = [
9
+ "Name",
10
+ "Fontname",
11
+ "Fontpath",
12
+ "Fontsize",
13
+ "PrimaryColour",
14
+ "SecondaryColour",
15
+ "OutlineColour",
16
+ "BackColour",
17
+ "Bold",
18
+ "Italic",
19
+ "Underline",
20
+ "StrikeOut",
21
+ "ScaleX",
22
+ "ScaleY",
23
+ "Spacing",
24
+ "Angle",
25
+ "BorderStyle",
26
+ "Outline",
27
+ "Shadow",
28
+ "Alignment",
29
+ "MarginL",
30
+ "MarginR",
31
+ "MarginV",
32
+ "Encoding",
33
+ ]
34
+
35
+ # Constructor
36
+ def __init__(self):
37
+ self.type = None
38
+ self.fake = False
39
+
40
+ self.Name = ""
41
+ self.Fontname = ""
42
+ self.Fontpath = ""
43
+ self.Fontsize = 1.0
44
+ self.PrimaryColour = (255, 255, 255, 255)
45
+ self.SecondaryColour = (255, 255, 255, 255)
46
+ self.OutlineColour = (255, 255, 255, 255)
47
+ self.BackColour = (255, 255, 255, 255)
48
+ self.Bold = False
49
+ self.Italic = False
50
+ self.Underline = False
51
+ self.StrikeOut = False
52
+ self.ScaleX = 100
53
+ self.ScaleY = 100
54
+ self.Spacing = 0
55
+ self.Angle = 0.0
56
+ self.BorderStyle = 1
57
+ self.Outline = 0
58
+ self.Shadow = 0
59
+ self.Alignment = ALIGN_BOTTOM_CENTER
60
+ self.MarginL = 0
61
+ self.MarginR = 0
62
+ self.MarginV = 0
63
+ self.Encoding = 0
64
+
65
+ def set(self, attribute_name, value, *args):
66
+ if hasattr(self, attribute_name):
67
+ if not attribute_name[0].isupper():
68
+ return
69
+ elif attribute_name in self.aliases:
70
+ attribute_name = self.aliases[attribute_name]
71
+ else:
72
+ return
73
+
74
+ setattr(self, attribute_name, self.formatters[attribute_name][0](value, *args))
75
+
76
+ def get(self, attribute_name, *args):
77
+ if hasattr(self, attribute_name):
78
+ if not attribute_name[0].isupper():
79
+ return None
80
+ elif attribute_name in self.aliases:
81
+ attribute_name = self.aliases[attribute_name]
82
+ else:
83
+ return None
84
+
85
+ return self.formatters[attribute_name][1](getattr(self, attribute_name), *args)
86
+
87
+ def copy(self, other=None):
88
+ if other is None:
89
+ # Creating a new style
90
+ other = self.__class__()
91
+ target = other
92
+ source = self
93
+ else:
94
+ # Copying into existing style
95
+ target = other # This was the issue - we had target and source swapped
96
+ source = self
97
+
98
+ # Copy all attributes
99
+ target.type = source.type
100
+ target.fake = source.fake # Also need to copy the fake flag
101
+
102
+ target.Name = source.Name
103
+ target.Fontname = source.Fontname
104
+ target.Fontpath = source.Fontpath
105
+ target.Fontsize = source.Fontsize
106
+ target.PrimaryColour = source.PrimaryColour
107
+ target.SecondaryColour = source.SecondaryColour
108
+ target.OutlineColour = source.OutlineColour
109
+ target.BackColour = source.BackColour
110
+ target.Bold = source.Bold
111
+ target.Italic = source.Italic
112
+ target.Underline = source.Underline
113
+ target.StrikeOut = source.StrikeOut
114
+ target.ScaleX = source.ScaleX
115
+ target.ScaleY = source.ScaleY
116
+ target.Spacing = source.Spacing
117
+ target.Angle = source.Angle
118
+ target.BorderStyle = source.BorderStyle
119
+ target.Outline = source.Outline
120
+ target.Shadow = source.Shadow
121
+ target.Alignment = source.Alignment
122
+ target.MarginL = source.MarginL
123
+ target.MarginR = source.MarginR
124
+ target.MarginV = source.MarginV
125
+ target.Encoding = source.Encoding
126
+
127
+ return target
128
+
129
+ def equals(self, other, names_can_differ=False):
130
+ return (
131
+ self.type == other.type
132
+ and not self.fake
133
+ and not other.fake
134
+ and not other.fake
135
+ and (names_can_differ or self.Name == other.Name)
136
+ and self.Fontname == other.Fontname
137
+ and self.Fontpath == other.Fontpath
138
+ and self.Fontsize == other.Fontsize
139
+ and self.PrimaryColour == other.PrimaryColour
140
+ and self.SecondaryColour == other.SecondaryColour
141
+ and self.OutlineColour == other.OutlineColour
142
+ and self.BackColour == other.BackColour
143
+ and self.Bold == other.Bold
144
+ and self.Italic == other.Italic
145
+ and self.Underline == other.Underline
146
+ and self.StrikeOut == other.StrikeOut
147
+ and self.ScaleX == other.ScaleX
148
+ and self.ScaleY == other.ScaleY
149
+ and self.Spacing == other.Spacing
150
+ and self.Angle == other.Angle
151
+ and self.BorderStyle == other.BorderStyle
152
+ and self.Outline == other.Outline
153
+ and self.Shadow == other.Shadow
154
+ and self.Alignment == other.Alignment
155
+ and self.MarginL == other.MarginL
156
+ and self.MarginR == other.MarginR
157
+ and self.MarginV == other.MarginV
158
+ and self.Encoding == other.Encoding
159
+ )
160
+
161
+
162
+ Style.formatters = {
163
+ "Name": (Formatters.same, Formatters.same),
164
+ "Fontname": (Formatters.same, Formatters.same),
165
+ "Fontpath": (Formatters.same, Formatters.same),
166
+ "Fontsize": (Formatters.str_to_number, Formatters.number_to_str),
167
+ "PrimaryColour": (Formatters.str_to_color, Formatters.color_to_str),
168
+ "SecondaryColour": (Formatters.str_to_color, Formatters.color_to_str),
169
+ "OutlineColour": (Formatters.str_to_color, Formatters.color_to_str),
170
+ "BackColour": (Formatters.str_to_color, Formatters.color_to_str),
171
+ "Bold": (Formatters.str_to_n1bool, Formatters.n1bool_to_str),
172
+ "Italic": (Formatters.str_to_n1bool, Formatters.n1bool_to_str),
173
+ "Underline": (Formatters.str_to_n1bool, Formatters.n1bool_to_str),
174
+ "StrikeOut": (Formatters.str_to_n1bool, Formatters.n1bool_to_str),
175
+ "ScaleX": (Formatters.str_to_integer, Formatters.integer_to_str),
176
+ "ScaleY": (Formatters.str_to_integer, Formatters.integer_to_str),
177
+ "Spacing": (Formatters.str_to_integer, Formatters.integer_to_str),
178
+ "Angle": (Formatters.str_to_number, Formatters.number_to_str),
179
+ "BorderStyle": (Formatters.str_to_integer, Formatters.integer_to_str),
180
+ "Outline": (Formatters.str_to_integer, Formatters.integer_to_str),
181
+ "Shadow": (Formatters.str_to_integer, Formatters.integer_to_str),
182
+ "Alignment": (Formatters.str_to_integer, Formatters.integer_to_str),
183
+ "MarginL": (Formatters.str_to_integer, Formatters.integer_to_str),
184
+ "MarginR": (Formatters.str_to_integer, Formatters.integer_to_str),
185
+ "MarginV": (Formatters.str_to_integer, Formatters.integer_to_str),
186
+ "Encoding": (Formatters.str_to_integer, Formatters.integer_to_str),
187
+ }