Skip to content

Commit

Permalink
Added tests for lyrics screens, further refactoring, deleting dupe code
Browse files Browse the repository at this point in the history
  • Loading branch information
beveradb committed Jan 19, 2025
1 parent 70719e2 commit 1c8a0a4
Show file tree
Hide file tree
Showing 17 changed files with 1,313 additions and 690 deletions.
21 changes: 21 additions & 0 deletions lyrics_transcriber/output/ass/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from lyrics_transcriber.output.ass.lyrics_screen import LyricsScreen
from lyrics_transcriber.output.ass.lyrics_line import LyricsLine
from lyrics_transcriber.output.ass.section_screen import SectionScreen
from lyrics_transcriber.output.ass.style import Style
from lyrics_transcriber.output.ass.event import Event
from lyrics_transcriber.output.ass.config import (
ScreenConfig,
LineTimingInfo,
LineState,
)

__all__ = [
'LyricsScreen',
'LyricsLine',
'SectionScreen',
'Style',
'Event',
'ScreenConfig',
'LineTimingInfo',
'LineState',
]
40 changes: 40 additions & 0 deletions lyrics_transcriber/output/ass/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from dataclasses import dataclass


@dataclass
class ScreenConfig:
"""Configuration for screen timing and layout."""

# Screen layout
max_visible_lines: int = 4
line_height: int = 50
top_padding: int = 50 # One line height of padding
video_height: int = 720 # 720p default

# Timing configuration
screen_gap_threshold: float = 5.0
post_roll_time: float = 1.0
fade_in_ms: int = 100
fade_out_ms: int = 400
cascade_delay_ms: int = 200
target_preshow_time: float = 5.0
position_clear_buffer_ms: int = 300


@dataclass
class LineTimingInfo:
"""Timing information for a single line."""

fade_in_time: float
end_time: float
fade_out_time: float
clear_time: float


@dataclass
class LineState:
"""Complete state for a single line."""

text: str
timing: LineTimingInfo
y_position: int
45 changes: 45 additions & 0 deletions lyrics_transcriber/output/ass/lyrics_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from dataclasses import dataclass
from typing import Optional
import logging
from datetime import timedelta

from lyrics_transcriber.types import LyricsSegment


@dataclass
class LyricsLine:
"""Represents a single line of lyrics with timing and karaoke information."""

segment: LyricsSegment
logger: Optional[logging.Logger] = None

def __post_init__(self):
"""Ensure logger is initialized"""
if self.logger is None:
self.logger = logging.getLogger(__name__)

def _create_ass_text(self, start_ts: timedelta) -> str:
"""Create the ASS text with karaoke timing tags."""
# Initial delay before first word
first_word_time = self.segment.start_time
start_time = max(0, (first_word_time - start_ts.total_seconds()) * 100)
text = r"{\k" + str(int(round(start_time))) + r"}"

prev_end_time = first_word_time

for word in self.segment.words:
# Add gap between words if needed
gap = word.start_time - prev_end_time
if gap > 0.1: # Only add gap if significant
text += r"{\k" + str(int(round(gap * 100))) + r"}"

# Add the word with its duration
duration = int(round((word.end_time - word.start_time) * 100))
text += r"{\kf" + str(duration) + r"}" + word.text + " "

prev_end_time = word.end_time # Track the actual end time of the word

return text.rstrip()

def __str__(self):
return f"{{{self.segment.text}}}"
5 changes: 0 additions & 5 deletions lyrics_transcriber/output/ass/lyrics_models/__init__.py

This file was deleted.

94 changes: 0 additions & 94 deletions lyrics_transcriber/output/ass/lyrics_models/lyrics_line.py

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,8 @@

from lyrics_transcriber.output.ass.style import Style
from lyrics_transcriber.output.ass.event import Event
from lyrics_transcriber.output.ass.lyrics_models.lyrics_line import LyricsLine


@dataclass
class ScreenConfig:
"""Configuration for screen timing and layout."""

# Screen layout
max_visible_lines: int = 4
line_height: int = 50
top_padding: int = 50 # One line height of padding
video_height: int = 720 # 720p default

# Timing configuration
screen_gap_threshold: float = 5.0
post_roll_time: float = 1.0
fade_in_ms: int = 100
fade_out_ms: int = 400
cascade_delay_ms: int = 200
target_preshow_time: float = 5.0
position_clear_buffer_ms: int = 300


@dataclass
class LineTimingInfo:
"""Timing information for a single line."""

fade_in_time: float
end_time: float
fade_out_time: float
clear_time: float


@dataclass
class LineState:
"""Complete state for a single line."""

text: str
timing: LineTimingInfo
y_position: int
from lyrics_transcriber.output.ass.lyrics_line import LyricsLine
from lyrics_transcriber.output.ass.config import ScreenConfig, LineTimingInfo, LineState


class PositionStrategy:
Expand Down Expand Up @@ -112,13 +74,13 @@ def calculate_line_timings(

# Check if we need to wait for previous lines to clear
if previous_active_lines:
top_lines = sorted([(end, pos, text) for end, pos, text in previous_active_lines], key=lambda x: x[1])[:2]
if top_lines:
latest_clear_time = max(
end + (self.config.fade_out_ms / 1000) + (self.config.position_clear_buffer_ms / 1000) for end, _, _ in top_lines
)
first_line_fade_in = max(first_line_fade_in, latest_clear_time)
self.logger.debug(f" Waiting for top lines to clear at {latest_clear_time:.2f}s")
# Calculate latest clear time from ALL previous lines
latest_clear_time = max(
end + (self.config.fade_out_ms / 1000) + (self.config.position_clear_buffer_ms / 1000)
for end, _, _ in previous_active_lines
)
first_line_fade_in = max(first_line_fade_in, latest_clear_time)
self.logger.debug(f" Waiting for lines to clear at {latest_clear_time:.2f}s")

timings = []
for i, line in enumerate(current_lines):
Expand Down
2 changes: 1 addition & 1 deletion lyrics_transcriber/output/ass/section_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging

from lyrics_transcriber.types import LyricsSegment
from lyrics_transcriber.output.ass.lyrics_models import LyricsScreen, SectionScreen
from lyrics_transcriber.output.ass import LyricsScreen, SectionScreen


class SectionDetector:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from lyrics_transcriber.output.ass.style import Style
from lyrics_transcriber.output.ass.event import Event
from lyrics_transcriber.output.ass.lyrics_models.lyrics_screen import ScreenConfig
from lyrics_transcriber.output.ass.lyrics_screen import ScreenConfig


@dataclass
Expand All @@ -21,55 +21,71 @@ class SectionScreen:
config: Optional[ScreenConfig] = None

def __post_init__(self):
self._initialize_logger_and_config()
self._calculate_duration()
self._adjust_timing()
self._create_text()

def _initialize_logger_and_config(self):
"""Initialize logger and config with defaults if not provided."""
if self.logger is None:
self.logger = logging.getLogger(__name__)
if self.config is None:
self.config = ScreenConfig(line_height=self.line_height)

# Update video height in config
self.config.video_height = self.video_size[1]

# Calculate actual duration in seconds (rounded to nearest second)
duration_secs = round(self.end_time - self.start_time)
def _calculate_duration(self):
"""Calculate duration before any timing adjustments."""
self.original_duration = round(self.end_time - self.start_time)

# Adjust timing for intro sections
def _adjust_timing(self):
"""Apply timing adjustments based on section type."""
if self.section_type == "INTRO":
self.start_time = 1.0 # Start after 1 second
self.end_time = self.end_time - 5.0 # End 5 seconds before next section

# Create a synthetic segment for the section marker
text = f"{self.section_type} ({duration_secs} seconds)"
self.text = text

def as_ass_events(
self,
style: Style,
next_screen_start: Optional[timedelta] = None,
previous_active_lines: List[Tuple[float, int, str]] = None,
) -> Tuple[List[Event], List[Tuple[float, int, str]]]:
"""Create ASS events for section markers with karaoke highlighting."""
self.logger.debug(f"Creating section marker event for {self.section_type}")
def _create_text(self):
"""Create the section text with duration."""
self.text = f"{self.section_type} ({self.original_duration} seconds)"

# Wait for previous lines to fade out
def _calculate_start_time(self, previous_active_lines: Optional[List[Tuple[float, int, str]]] = None) -> float:
"""Calculate start time accounting for previous lines."""
start_time = self.start_time
if previous_active_lines:
latest_end = max(end + (self.config.fade_out_ms / 1000) for end, _, _ in previous_active_lines)
start_time = max(start_time, latest_end)
return start_time

def _calculate_vertical_position(self) -> int:
"""Calculate vertical position for centered text."""
return (self.video_size[1] - self.line_height) // 2

def _create_event(self, style: Style, start_time: float) -> Event:
"""Create an ASS event with proper formatting."""
event = Event()
event.type = "Dialogue"
event.Layer = 0
event.Style = style
event.Start = start_time
event.End = self.end_time

# Calculate vertical position (centered on screen)
y_position = (self.video_size[1] - self.line_height) // 2
event.MarginV = y_position
event.MarginV = self._calculate_vertical_position()

# Add karaoke timing for the entire duration
duration = int((self.end_time - self.start_time) * 100) # Convert to centiseconds
event.Text = f"{{\\fad(300,300)}}{{\\an8}}{{\\K{duration}}}{self.text}"
duration = int((self.end_time - start_time) * 100) # Convert to centiseconds
event.Text = f"{{\\fad({self.config.fade_in_ms},{self.config.fade_out_ms})}}" f"{{\\an8}}{{\\K{duration}}}{self.text}"
return event

def as_ass_events(
self,
style: Style,
next_screen_start: Optional[timedelta] = None,
previous_active_lines: List[Tuple[float, int, str]] = None,
) -> Tuple[List[Event], List[Tuple[float, int, str]]]:
"""Create ASS events for section markers with karaoke highlighting."""
self.logger.debug(f"Creating section marker event for {self.section_type}")

start_time = self._calculate_start_time(previous_active_lines)
event = self._create_event(style, start_time)

self.logger.debug(f"Created section event: {event.Text} ({event.Start}s - {event.End}s)")
return [event], [] # No active lines to track for sections
Expand Down
Loading

0 comments on commit 1c8a0a4

Please sign in to comment.