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,33 +1,18 @@
|
|
1
1
|
from dataclasses import dataclass
|
2
2
|
import os
|
3
3
|
import logging
|
4
|
-
from typing import
|
5
|
-
import
|
6
|
-
from datetime import timedelta
|
4
|
+
from typing import List, Optional
|
5
|
+
import json
|
7
6
|
|
8
|
-
from lyrics_transcriber.
|
9
|
-
from .
|
10
|
-
from
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
output_dir: str
|
18
|
-
cache_dir: str
|
19
|
-
video_resolution: str = "360p"
|
20
|
-
video_background_image: Optional[str] = None
|
21
|
-
video_background_color: str = "black"
|
22
|
-
|
23
|
-
def __post_init__(self):
|
24
|
-
"""Validate configuration after initialization."""
|
25
|
-
if not self.output_dir:
|
26
|
-
raise ValueError("output_dir must be provided")
|
27
|
-
if not self.cache_dir:
|
28
|
-
raise ValueError("cache_dir must be provided")
|
29
|
-
if self.video_background_image and not os.path.isfile(self.video_background_image):
|
30
|
-
raise FileNotFoundError(f"Video background image not found: {self.video_background_image}")
|
7
|
+
from lyrics_transcriber.types import LyricsData
|
8
|
+
from lyrics_transcriber.correction.corrector import CorrectionResult
|
9
|
+
from lyrics_transcriber.output.plain_text import PlainTextGenerator
|
10
|
+
from lyrics_transcriber.output.lyrics_file import LyricsFileGenerator
|
11
|
+
from lyrics_transcriber.output.subtitles import SubtitlesGenerator
|
12
|
+
from lyrics_transcriber.output.video import VideoGenerator
|
13
|
+
from lyrics_transcriber.output.segment_resizer import SegmentResizer
|
14
|
+
from lyrics_transcriber.output.cdg import CDGGenerator
|
15
|
+
from lyrics_transcriber.core.config import OutputConfig
|
31
16
|
|
32
17
|
|
33
18
|
@dataclass
|
@@ -37,6 +22,12 @@ class OutputPaths:
|
|
37
22
|
lrc: Optional[str] = None
|
38
23
|
ass: Optional[str] = None
|
39
24
|
video: Optional[str] = None
|
25
|
+
original_txt: Optional[str] = None
|
26
|
+
corrected_txt: Optional[str] = None
|
27
|
+
corrections_json: Optional[str] = None
|
28
|
+
cdg: Optional[str] = None
|
29
|
+
mp3: Optional[str] = None
|
30
|
+
cdg_zip: Optional[str] = None
|
40
31
|
|
41
32
|
|
42
33
|
class OutputGenerator:
|
@@ -44,33 +35,72 @@ class OutputGenerator:
|
|
44
35
|
|
45
36
|
def __init__(
|
46
37
|
self,
|
47
|
-
config:
|
38
|
+
config: OutputConfig,
|
48
39
|
logger: Optional[logging.Logger] = None,
|
49
40
|
):
|
50
41
|
"""
|
51
42
|
Initialize OutputGenerator with configuration.
|
52
43
|
|
53
44
|
Args:
|
54
|
-
config:
|
45
|
+
config: OutputConfig instance with required paths and settings
|
55
46
|
logger: Optional logger instance
|
56
47
|
"""
|
57
48
|
self.config = config
|
58
49
|
self.logger = logger or logging.getLogger(__name__)
|
59
50
|
|
60
|
-
|
61
|
-
self.logger.debug(f"Initialized OutputGenerator with output_dir: {self.config.output_dir}")
|
62
|
-
self.logger.debug(f"Using cache_dir: {self.config.cache_dir}")
|
51
|
+
self.logger.debug(f"Initializing OutputGenerator with config: {self.config}")
|
63
52
|
|
64
53
|
# Set video resolution parameters
|
65
54
|
self.video_resolution_num, self.font_size, self.line_height = self._get_video_params(self.config.video_resolution)
|
66
55
|
|
56
|
+
self.segment_resizer = SegmentResizer(max_line_length=self.config.max_line_length, logger=self.logger)
|
57
|
+
|
58
|
+
# Initialize generators
|
59
|
+
self.plain_text = PlainTextGenerator(self.config.output_dir, self.logger)
|
60
|
+
self.lyrics_file = LyricsFileGenerator(self.config.output_dir, self.logger)
|
61
|
+
|
62
|
+
if self.config.render_video or self.config.generate_cdg:
|
63
|
+
# Load output styles from JSON
|
64
|
+
try:
|
65
|
+
with open(self.config.output_styles_json, "r") as f:
|
66
|
+
self.config.styles = json.load(f)
|
67
|
+
self.logger.debug(f"Loaded output styles from: {self.config.output_styles_json}")
|
68
|
+
except Exception as e:
|
69
|
+
raise ValueError(f"Failed to load output styles file: {str(e)}")
|
70
|
+
|
71
|
+
if self.config.generate_cdg:
|
72
|
+
self.cdg = CDGGenerator(self.config.output_dir, self.logger)
|
73
|
+
|
74
|
+
if self.config.render_video:
|
75
|
+
self.subtitle = SubtitlesGenerator(
|
76
|
+
output_dir=self.config.output_dir,
|
77
|
+
video_resolution=self.video_resolution_num,
|
78
|
+
font_size=self.font_size,
|
79
|
+
line_height=self.line_height,
|
80
|
+
styles=self.config.styles,
|
81
|
+
logger=self.logger,
|
82
|
+
)
|
83
|
+
|
84
|
+
self.video = VideoGenerator(
|
85
|
+
output_dir=self.config.output_dir,
|
86
|
+
cache_dir=self.config.cache_dir,
|
87
|
+
video_resolution=self.video_resolution_num,
|
88
|
+
styles=self.config.styles,
|
89
|
+
logger=self.logger,
|
90
|
+
)
|
91
|
+
|
92
|
+
# Log the configured directories
|
93
|
+
self.logger.debug(f"Initialized OutputGenerator with output_dir: {self.config.output_dir}")
|
94
|
+
self.logger.debug(f"Using cache_dir: {self.config.cache_dir}")
|
95
|
+
|
67
96
|
def generate_outputs(
|
68
97
|
self,
|
69
98
|
transcription_corrected: CorrectionResult,
|
70
99
|
lyrics_results: List[LyricsData],
|
71
100
|
output_prefix: str,
|
72
101
|
audio_filepath: str,
|
73
|
-
|
102
|
+
artist: Optional[str] = None,
|
103
|
+
title: Optional[str] = None,
|
74
104
|
) -> OutputPaths:
|
75
105
|
"""Generate all requested output formats."""
|
76
106
|
outputs = OutputPaths()
|
@@ -78,122 +108,50 @@ class OutputGenerator:
|
|
78
108
|
try:
|
79
109
|
# Generate plain lyrics files for each provider
|
80
110
|
for lyrics_data in lyrics_results:
|
81
|
-
|
82
|
-
self.write_plain_lyrics(lyrics_data, f"{output_prefix} (Lyrics {provider_name})")
|
111
|
+
self.plain_text.write_lyrics(lyrics_data, output_prefix)
|
83
112
|
|
84
|
-
|
85
|
-
|
86
|
-
self.write_plain_lyrics_from_correction(transcription_corrected, f"{output_prefix} (Lyrics Corrected)")
|
113
|
+
# Write original (uncorrected) transcription
|
114
|
+
outputs.original_txt = self.plain_text.write_original_transcription(transcription_corrected, output_prefix)
|
87
115
|
|
88
|
-
|
89
|
-
|
116
|
+
# Resize corrected segments to ensure none are longer than max_line_length
|
117
|
+
resized_segments = self.segment_resizer.resize_segments(transcription_corrected.corrected_segments)
|
118
|
+
transcription_corrected.resized_segments = resized_segments
|
119
|
+
outputs.corrections_json = self.write_corrections_data(transcription_corrected, output_prefix)
|
90
120
|
|
91
|
-
|
92
|
-
|
121
|
+
# Write corrected lyrics as plain text
|
122
|
+
outputs.corrected_txt = self.plain_text.write_corrected_lyrics(resized_segments, output_prefix)
|
93
123
|
|
94
|
-
|
95
|
-
|
96
|
-
outputs.video = self.generate_video(outputs.ass, audio_filepath, output_prefix)
|
124
|
+
# Generate LRC using LyricsFileGenerator
|
125
|
+
outputs.lrc = self.lyrics_file.generate_lrc(resized_segments, output_prefix)
|
97
126
|
|
98
|
-
|
99
|
-
self.
|
100
|
-
|
127
|
+
# Generate CDG file if requested
|
128
|
+
if self.config.generate_cdg:
|
129
|
+
outputs.cdg, outputs.mp3, outputs.cdg_zip = self.cdg.generate_cdg(
|
130
|
+
segments=resized_segments,
|
131
|
+
audio_file=audio_filepath,
|
132
|
+
title=title or output_prefix,
|
133
|
+
artist=artist or "",
|
134
|
+
cdg_styles=self.config.styles["cdg"],
|
135
|
+
)
|
101
136
|
|
102
|
-
|
137
|
+
# Generate video if requested
|
138
|
+
if self.config.render_video:
|
139
|
+
# Generate ASS subtitles
|
140
|
+
outputs.ass = self.subtitle.generate_ass(resized_segments, output_prefix, audio_filepath)
|
141
|
+
outputs.video = self.video.generate_video(outputs.ass, audio_filepath, output_prefix)
|
103
142
|
|
104
|
-
|
105
|
-
"""Generate full output path for a file."""
|
106
|
-
return os.path.join(self.config.output_dir or self.config.cache_dir, f"{output_prefix}.{extension}")
|
107
|
-
|
108
|
-
def generate_lrc(self, transcription_data: CorrectionResult, output_prefix: str) -> str:
|
109
|
-
"""Generate LRC format lyrics file."""
|
110
|
-
self.logger.info("Generating LRC format lyrics")
|
111
|
-
output_path = self._get_output_path(output_prefix, "lrc")
|
112
|
-
|
113
|
-
try:
|
114
|
-
self._write_lrc_file(output_path, transcription_data.segments)
|
115
|
-
self.logger.info(f"LRC file generated: {output_path}")
|
116
|
-
return output_path
|
143
|
+
return outputs
|
117
144
|
|
118
145
|
except Exception as e:
|
119
|
-
self.logger.error(f"Failed to generate
|
146
|
+
self.logger.error(f"Failed to generate outputs: {str(e)}")
|
120
147
|
raise
|
121
148
|
|
122
|
-
def
|
123
|
-
"""
|
124
|
-
|
125
|
-
for segment in segments:
|
126
|
-
start_time = self._format_lrc_timestamp(segment.start_time)
|
127
|
-
line = f"[{start_time}]{segment.text}\n"
|
128
|
-
f.write(line)
|
129
|
-
|
130
|
-
def generate_ass(self, transcription_data: CorrectionResult, output_prefix: str) -> str:
|
131
|
-
"""Generate ASS format subtitles file."""
|
132
|
-
self.logger.info("Generating ASS format subtitles")
|
133
|
-
output_path = self._get_output_path(output_prefix, "ass")
|
134
|
-
|
135
|
-
try:
|
136
|
-
self._write_ass_file(output_path, transcription_data.segments)
|
137
|
-
self.logger.info(f"ASS file generated: {output_path}")
|
138
|
-
return output_path
|
139
|
-
|
140
|
-
except Exception as e:
|
141
|
-
self.logger.error(f"Failed to generate ASS file: {str(e)}")
|
142
|
-
raise
|
143
|
-
|
144
|
-
def _write_ass_file(self, output_path: str, segments: list) -> None:
|
145
|
-
"""Write ASS file content."""
|
146
|
-
with open(output_path, "w", encoding="utf-8") as f:
|
147
|
-
f.write(self._get_ass_header())
|
148
|
-
for segment in segments:
|
149
|
-
# Change from ts/end_ts to start_time/end_time
|
150
|
-
start_time = self._format_ass_timestamp(segment.start_time)
|
151
|
-
end_time = self._format_ass_timestamp(segment.end_time)
|
152
|
-
line = f"Dialogue: 0,{start_time},{end_time},Default,,0,0,0,,{segment.text}\n"
|
153
|
-
f.write(line)
|
154
|
-
|
155
|
-
def generate_video(self, ass_path: str, audio_path: str, output_prefix: str) -> str:
|
156
|
-
"""Generate MP4 video with lyrics overlay."""
|
157
|
-
self.logger.info("Generating video with lyrics overlay")
|
158
|
-
output_path = self._get_output_path(output_prefix, "mp4")
|
159
|
-
|
160
|
-
try:
|
161
|
-
cmd = self._build_ffmpeg_command(ass_path, audio_path, output_path)
|
162
|
-
self._run_ffmpeg_command(cmd)
|
163
|
-
self.logger.info(f"Video generated: {output_path}")
|
164
|
-
return output_path
|
165
|
-
|
166
|
-
except Exception as e:
|
167
|
-
self.logger.error(f"Failed to generate video: {str(e)}")
|
168
|
-
raise
|
169
|
-
|
170
|
-
def _build_ffmpeg_command(self, ass_path: str, audio_path: str, output_path: str) -> list:
|
171
|
-
"""Build FFmpeg command for video generation."""
|
172
|
-
width, height = self.video_resolution_num
|
173
|
-
cmd = ["ffmpeg", "-y"]
|
174
|
-
|
175
|
-
# Input source (background)
|
176
|
-
if self.config.video_background_image:
|
177
|
-
cmd.extend(["-i", self.config.video_background_image])
|
178
|
-
else:
|
179
|
-
cmd.extend(["-f", "lavfi", "-i", f"color=c={self.config.video_background_color}:s={width}x{height}"])
|
180
|
-
|
181
|
-
# Add audio and subtitle inputs
|
182
|
-
cmd.extend(["-i", audio_path, "-vf", f"ass={ass_path}", "-c:v", "libx264", "-c:a", "aac", "-shortest", output_path])
|
183
|
-
|
184
|
-
return cmd
|
185
|
-
|
186
|
-
def _run_ffmpeg_command(self, cmd: list) -> None:
|
187
|
-
"""Execute FFmpeg command."""
|
188
|
-
self.logger.debug(f"Running FFmpeg command: {' '.join(cmd)}")
|
189
|
-
try:
|
190
|
-
subprocess.run(cmd, check=True)
|
191
|
-
except subprocess.CalledProcessError as e:
|
192
|
-
self.logger.error(f"FFmpeg error: {str(e)}")
|
193
|
-
raise
|
149
|
+
def _get_output_path(self, output_prefix: str, extension: str) -> str:
|
150
|
+
"""Generate full output path for a file."""
|
151
|
+
return os.path.join(self.config.output_dir or self.config.cache_dir, f"{output_prefix}.{extension}")
|
194
152
|
|
195
153
|
def _get_video_params(self, resolution: str) -> tuple:
|
196
|
-
"""Get video parameters based on resolution
|
154
|
+
"""Get video parameters: (width, height), font_size, line_height based on video resolution config."""
|
197
155
|
match resolution:
|
198
156
|
case "4k":
|
199
157
|
return (3840, 2160), 250, 250
|
@@ -202,70 +160,20 @@ class OutputGenerator:
|
|
202
160
|
case "720p":
|
203
161
|
return (1280, 720), 100, 100
|
204
162
|
case "360p":
|
205
|
-
return (640, 360),
|
163
|
+
return (640, 360), 40, 50
|
206
164
|
case _:
|
207
165
|
raise ValueError("Invalid video_resolution value. Must be one of: 4k, 1080p, 720p, 360p")
|
208
166
|
|
209
|
-
def
|
210
|
-
"""
|
211
|
-
|
212
|
-
|
213
|
-
seconds = time.total_seconds() % 60
|
214
|
-
return f"{minutes:02d}:{seconds:05.2f}"
|
215
|
-
|
216
|
-
def _format_ass_timestamp(self, seconds: float) -> str:
|
217
|
-
"""Format timestamp for ASS format."""
|
218
|
-
time = timedelta(seconds=seconds)
|
219
|
-
hours = int(time.total_seconds() / 3600)
|
220
|
-
minutes = int((time.total_seconds() % 3600) / 60)
|
221
|
-
seconds = time.total_seconds() % 60
|
222
|
-
centiseconds = int((seconds % 1) * 100)
|
223
|
-
seconds = int(seconds)
|
224
|
-
return f"{hours}:{minutes:02d}:{seconds:02d}.{centiseconds:02d}"
|
225
|
-
|
226
|
-
def _get_ass_header(self) -> str:
|
227
|
-
"""Get ASS format header with style definitions."""
|
228
|
-
width, height = self.video_resolution_num
|
229
|
-
return f"""[Script Info]
|
230
|
-
ScriptType: v4.00+
|
231
|
-
PlayResX: {width}
|
232
|
-
PlayResY: {height}
|
233
|
-
WrapStyle: 0
|
234
|
-
|
235
|
-
[V4+ Styles]
|
236
|
-
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
237
|
-
Style: Default,Arial,{self.font_size},&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1
|
238
|
-
|
239
|
-
[Events]
|
240
|
-
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
241
|
-
"""
|
242
|
-
|
243
|
-
def write_plain_lyrics(self, lyrics_data: LyricsData, output_prefix: str) -> str:
|
244
|
-
"""Write plain text lyrics file."""
|
245
|
-
self.logger.info("Writing plain lyrics file")
|
246
|
-
output_path = self._get_output_path(output_prefix, "txt")
|
167
|
+
def write_corrections_data(self, correction_result: CorrectionResult, output_prefix: str) -> str:
|
168
|
+
"""Write corrections data to JSON file."""
|
169
|
+
self.logger.info("Writing corrections data JSON")
|
170
|
+
output_path = self._get_output_path(f"{output_prefix} (Lyrics Corrections)", "json")
|
247
171
|
|
248
172
|
try:
|
249
173
|
with open(output_path, "w", encoding="utf-8") as f:
|
250
|
-
|
251
|
-
self.logger.info(f"
|
174
|
+
json.dump(correction_result.to_dict(), f, indent=2, ensure_ascii=False)
|
175
|
+
self.logger.info(f"Corrections data JSON generated: {output_path}")
|
252
176
|
return output_path
|
253
|
-
|
254
|
-
except Exception as e:
|
255
|
-
self.logger.error(f"Failed to write plain lyrics file: {str(e)}")
|
256
|
-
raise
|
257
|
-
|
258
|
-
def write_plain_lyrics_from_correction(self, correction_result: CorrectionResult, output_prefix: str) -> str:
|
259
|
-
"""Write corrected lyrics as plain text file."""
|
260
|
-
self.logger.info("Writing corrected lyrics file")
|
261
|
-
output_path = self._get_output_path(output_prefix, "txt")
|
262
|
-
|
263
|
-
try:
|
264
|
-
with open(output_path, "w", encoding="utf-8") as f:
|
265
|
-
f.write(correction_result.text)
|
266
|
-
self.logger.info(f"Corrected lyrics file generated: {output_path}")
|
267
|
-
return output_path
|
268
|
-
|
269
177
|
except Exception as e:
|
270
|
-
self.logger.error(f"Failed to write
|
178
|
+
self.logger.error(f"Failed to write corrections data JSON: {str(e)}")
|
271
179
|
raise
|
@@ -0,0 +1,102 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
from typing import List, Optional
|
4
|
+
|
5
|
+
from lyrics_transcriber.types import LyricsSegment, Word
|
6
|
+
|
7
|
+
|
8
|
+
class LyricsFileGenerator:
|
9
|
+
"""Handles generation of lyrics files in various formats (LRC, etc)."""
|
10
|
+
|
11
|
+
def __init__(self, output_dir: str, logger: Optional[logging.Logger] = None):
|
12
|
+
"""Initialize LyricsFileGenerator.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
output_dir: Directory where output files will be written
|
16
|
+
logger: Optional logger instance
|
17
|
+
"""
|
18
|
+
self.output_dir = output_dir
|
19
|
+
self.logger = logger or logging.getLogger(__name__)
|
20
|
+
|
21
|
+
def _get_output_path(self, output_prefix: str, extension: str) -> str:
|
22
|
+
"""Generate full output path for a file."""
|
23
|
+
return os.path.join(self.output_dir, f"{output_prefix}.{extension}")
|
24
|
+
|
25
|
+
def generate_lrc(self, segments: List[LyricsSegment], output_prefix: str) -> str:
|
26
|
+
"""Generate LRC format lyrics file.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
segments: List of LyricsSegment objects containing word timing data
|
30
|
+
output_prefix: Prefix for output filename
|
31
|
+
|
32
|
+
Returns:
|
33
|
+
Path to generated LRC file
|
34
|
+
"""
|
35
|
+
self.logger.info("Generating LRC format lyrics")
|
36
|
+
output_path = self._get_output_path(f"{output_prefix} (Karaoke)", "lrc")
|
37
|
+
|
38
|
+
try:
|
39
|
+
self._write_lrc_file(output_path, segments)
|
40
|
+
self.logger.info(f"LRC file generated: {output_path}")
|
41
|
+
return output_path
|
42
|
+
|
43
|
+
except Exception as e:
|
44
|
+
self.logger.error(f"Failed to generate LRC file: {str(e)}")
|
45
|
+
raise
|
46
|
+
|
47
|
+
def _write_lrc_file(self, output_path: str, segments: List[LyricsSegment]) -> None:
|
48
|
+
"""Write LRC file content with MidiCo-compatible word-level timestamps.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
output_path: Path to write the LRC file
|
52
|
+
segments: List of LyricsSegment objects containing word timing data
|
53
|
+
"""
|
54
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
55
|
+
# Write MidiCo header
|
56
|
+
f.write("[re:MidiCo]\n")
|
57
|
+
|
58
|
+
for segment in segments:
|
59
|
+
for i, word in enumerate(segment.words):
|
60
|
+
start_time = self._format_lrc_timestamp(word.start_time)
|
61
|
+
|
62
|
+
# Add space after all words except last in segment
|
63
|
+
text = word.text
|
64
|
+
if i != len(segment.words) - 1:
|
65
|
+
text += " "
|
66
|
+
|
67
|
+
# Add "/" prefix for first word in segment
|
68
|
+
prefix = "/" if i == 0 else ""
|
69
|
+
|
70
|
+
# Write MidiCo formatted line
|
71
|
+
f.write(f"[{start_time}]1:{prefix}{text}\n")
|
72
|
+
|
73
|
+
def _format_lrc_timestamp(self, seconds: float) -> str:
|
74
|
+
"""Format timestamp for MidiCo LRC format (MM:SS.mmm).
|
75
|
+
|
76
|
+
Args:
|
77
|
+
seconds: Time in seconds
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
Formatted timestamp string in MM:SS.mmm format
|
81
|
+
"""
|
82
|
+
minutes = int(seconds // 60)
|
83
|
+
remaining_seconds = seconds % 60
|
84
|
+
|
85
|
+
# Convert to milliseconds and round to nearest integer
|
86
|
+
total_milliseconds = round(remaining_seconds * 1000)
|
87
|
+
|
88
|
+
# Extract seconds and milliseconds
|
89
|
+
seconds_part = total_milliseconds // 1000
|
90
|
+
milliseconds = total_milliseconds % 1000
|
91
|
+
|
92
|
+
# Handle rollover
|
93
|
+
if seconds_part == 60:
|
94
|
+
seconds_part = 0
|
95
|
+
minutes += 1
|
96
|
+
|
97
|
+
return f"{minutes:02d}:{seconds_part:02d}.{milliseconds:03d}"
|
98
|
+
|
99
|
+
# Future methods for other lyrics file formats can be added here
|
100
|
+
# def generate_txt(self, segments: List[LyricsSegment], output_prefix: str) -> str:
|
101
|
+
# """Generate Power Karaoke TXT format lyrics file."""
|
102
|
+
# pass
|
@@ -0,0 +1,91 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
from typing import List, Optional
|
4
|
+
|
5
|
+
from lyrics_transcriber.types import LyricsData, LyricsSegment
|
6
|
+
from lyrics_transcriber.correction.corrector import CorrectionResult
|
7
|
+
|
8
|
+
class PlainTextGenerator:
|
9
|
+
"""Handles generation of plain text output files for lyrics and transcriptions."""
|
10
|
+
|
11
|
+
def __init__(self, output_dir: str, logger: Optional[logging.Logger] = None):
|
12
|
+
"""Initialize PlainTextGenerator.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
output_dir: Directory where output files will be written
|
16
|
+
logger: Optional logger instance
|
17
|
+
"""
|
18
|
+
self.output_dir = output_dir
|
19
|
+
self.logger = logger or logging.getLogger(__name__)
|
20
|
+
|
21
|
+
def _get_output_path(self, output_prefix: str, extension: str) -> str:
|
22
|
+
"""Generate full output path for a file."""
|
23
|
+
return os.path.join(self.output_dir, f"{output_prefix}.{extension}")
|
24
|
+
|
25
|
+
def write_lyrics(self, lyrics_data: LyricsData, output_prefix: str) -> str:
|
26
|
+
"""Write plain text lyrics file from provider data.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
lyrics_data: LyricsData from a lyrics provider
|
30
|
+
output_prefix: Prefix for output filename
|
31
|
+
|
32
|
+
Returns:
|
33
|
+
Path to generated file
|
34
|
+
"""
|
35
|
+
self.logger.info("Writing plain lyrics file")
|
36
|
+
provider_name = lyrics_data.metadata.source.title()
|
37
|
+
output_path = self._get_output_path(f"{output_prefix} (Lyrics {provider_name})", "txt")
|
38
|
+
|
39
|
+
try:
|
40
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
41
|
+
f.write(lyrics_data.lyrics)
|
42
|
+
self.logger.info(f"Plain lyrics file generated: {output_path}")
|
43
|
+
return output_path
|
44
|
+
except Exception as e:
|
45
|
+
self.logger.error(f"Failed to write plain lyrics file: {str(e)}")
|
46
|
+
raise
|
47
|
+
|
48
|
+
def write_corrected_lyrics(self, segments: List[LyricsSegment], output_prefix: str) -> str:
|
49
|
+
"""Write corrected lyrics as plain text file.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
segments: List of corrected LyricsSegment objects
|
53
|
+
output_prefix: Prefix for output filename
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
Path to generated file
|
57
|
+
"""
|
58
|
+
self.logger.info("Writing corrected lyrics file")
|
59
|
+
output_path = self._get_output_path(f"{output_prefix} (Lyrics Corrected)", "txt")
|
60
|
+
|
61
|
+
try:
|
62
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
63
|
+
for segment in segments:
|
64
|
+
f.write(f"{segment.text}\n")
|
65
|
+
self.logger.info(f"Corrected lyrics file generated: {output_path}")
|
66
|
+
return output_path
|
67
|
+
except Exception as e:
|
68
|
+
self.logger.error(f"Failed to write corrected lyrics file: {str(e)}")
|
69
|
+
raise
|
70
|
+
|
71
|
+
def write_original_transcription(self, correction_result: CorrectionResult, output_prefix: str) -> str:
|
72
|
+
"""Write original (uncorrected) transcription as plain text.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
correction_result: CorrectionResult containing original transcription
|
76
|
+
output_prefix: Prefix for output filename
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
Path to generated file
|
80
|
+
"""
|
81
|
+
self.logger.info("Writing original transcription file")
|
82
|
+
output_path = self._get_output_path(f"{output_prefix} (Lyrics Uncorrected)", "txt")
|
83
|
+
|
84
|
+
try:
|
85
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
86
|
+
f.write(correction_result.transcribed_text)
|
87
|
+
self.logger.info(f"Original transcription file generated: {output_path}")
|
88
|
+
return output_path
|
89
|
+
except Exception as e:
|
90
|
+
self.logger.error(f"Failed to write original transcription file: {str(e)}")
|
91
|
+
raise
|