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.
- lyrics_transcriber/__init__.py +2 -1
- lyrics_transcriber/cli/{main.py → cli_main.py} +47 -14
- lyrics_transcriber/core/config.py +35 -0
- lyrics_transcriber/core/controller.py +164 -166
- lyrics_transcriber/correction/anchor_sequence.py +471 -0
- lyrics_transcriber/correction/corrector.py +256 -0
- 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 +125 -0
- lyrics_transcriber/lyrics/genius.py +73 -0
- lyrics_transcriber/lyrics/spotify.py +82 -0
- 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 +140 -171
- 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/storage/dropbox.py +110 -134
- lyrics_transcriber/transcribers/audioshake.py +171 -105
- lyrics_transcriber/transcribers/base_transcriber.py +149 -0
- lyrics_transcriber/transcribers/whisper.py +267 -133
- lyrics_transcriber/types.py +454 -0
- {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/METADATA +14 -3
- lyrics_transcriber-0.32.1.dist-info/RECORD +86 -0
- {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/WHEEL +1 -1
- lyrics_transcriber-0.32.1.dist-info/entry_points.txt +4 -0
- lyrics_transcriber/core/corrector.py +0 -56
- lyrics_transcriber/core/fetcher.py +0 -143
- lyrics_transcriber/storage/tokens.py +0 -116
- lyrics_transcriber/transcribers/base.py +0 -31
- lyrics_transcriber-0.30.0.dist-info/RECORD +0 -22
- lyrics_transcriber-0.30.0.dist-info/entry_points.txt +0 -3
- {lyrics_transcriber-0.30.0.dist-info → lyrics_transcriber-0.32.1.dist-info}/LICENSE +0 -0
@@ -1,9 +1,33 @@
|
|
1
|
+
from dataclasses import dataclass
|
1
2
|
import os
|
2
3
|
import logging
|
3
|
-
from typing import
|
4
|
-
import
|
5
|
-
|
6
|
-
from .
|
4
|
+
from typing import List, Optional
|
5
|
+
import json
|
6
|
+
|
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
|
16
|
+
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class OutputPaths:
|
20
|
+
"""Holds paths for generated output files."""
|
21
|
+
|
22
|
+
lrc: Optional[str] = None
|
23
|
+
ass: Optional[str] = None
|
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
|
7
31
|
|
8
32
|
|
9
33
|
class OutputGenerator:
|
@@ -11,158 +35,123 @@ class OutputGenerator:
|
|
11
35
|
|
12
36
|
def __init__(
|
13
37
|
self,
|
38
|
+
config: OutputConfig,
|
14
39
|
logger: Optional[logging.Logger] = None,
|
15
|
-
output_dir: Optional[str] = None,
|
16
|
-
cache_dir: str = "/tmp/lyrics-transcriber-cache/",
|
17
|
-
video_resolution: str = "360p",
|
18
|
-
video_background_image: Optional[str] = None,
|
19
|
-
video_background_color: str = "black",
|
20
40
|
):
|
21
|
-
self.logger = logger or logging.getLogger(__name__)
|
22
|
-
self.output_dir = output_dir
|
23
|
-
self.cache_dir = cache_dir
|
24
|
-
|
25
|
-
# Video settings
|
26
|
-
self.video_resolution = video_resolution
|
27
|
-
self.video_background_image = video_background_image
|
28
|
-
self.video_background_color = video_background_color
|
29
|
-
|
30
|
-
# Set video resolution parameters
|
31
|
-
self.video_resolution_num, self.font_size, self.line_height = self._get_video_params(video_resolution)
|
32
|
-
|
33
|
-
# Validate video background if provided
|
34
|
-
if self.video_background_image and not os.path.isfile(self.video_background_image):
|
35
|
-
raise FileNotFoundError(f"Video background image not found: {self.video_background_image}")
|
36
|
-
|
37
|
-
def generate_outputs(
|
38
|
-
self, transcription_data: Dict[str, Any], output_prefix: str, audio_filepath: str, render_video: bool = False
|
39
|
-
) -> Dict[str, str]:
|
40
41
|
"""
|
41
|
-
|
42
|
+
Initialize OutputGenerator with configuration.
|
42
43
|
|
43
44
|
Args:
|
44
|
-
|
45
|
-
|
46
|
-
audio_filepath: Path to the source audio file
|
47
|
-
render_video: Whether to generate video output
|
48
|
-
|
49
|
-
Returns:
|
50
|
-
Dictionary of output paths for each format
|
45
|
+
config: OutputConfig instance with required paths and settings
|
46
|
+
logger: Optional logger instance
|
51
47
|
"""
|
52
|
-
|
53
|
-
|
54
|
-
try:
|
55
|
-
# Generate LRC
|
56
|
-
lrc_path = self.generate_lrc(transcription_data, output_prefix)
|
57
|
-
outputs["lrc"] = lrc_path
|
58
|
-
|
59
|
-
# Generate ASS
|
60
|
-
ass_path = self.generate_ass(transcription_data, output_prefix)
|
61
|
-
outputs["ass"] = ass_path
|
62
|
-
|
63
|
-
# Generate video if requested
|
64
|
-
if render_video:
|
65
|
-
video_path = self.generate_video(ass_path, audio_filepath, output_prefix)
|
66
|
-
outputs["video"] = video_path
|
67
|
-
|
68
|
-
except Exception as e:
|
69
|
-
self.logger.error(f"Error generating outputs: {str(e)}")
|
70
|
-
raise
|
71
|
-
|
72
|
-
return outputs
|
73
|
-
|
74
|
-
def generate_lrc(self, transcription_data: Dict[str, Any], output_prefix: str) -> str:
|
75
|
-
"""Generate LRC format lyrics file."""
|
76
|
-
self.logger.info("Generating LRC format lyrics")
|
77
|
-
|
78
|
-
output_path = os.path.join(self.output_dir or self.cache_dir, f"{output_prefix}.lrc")
|
79
|
-
|
80
|
-
try:
|
81
|
-
with open(output_path, "w", encoding="utf-8") as f:
|
82
|
-
for segment in transcription_data["segments"]:
|
83
|
-
start_time = self._format_lrc_timestamp(segment["start"])
|
84
|
-
line = f"[{start_time}]{segment['text']}\n"
|
85
|
-
f.write(line)
|
86
|
-
|
87
|
-
self.logger.info(f"LRC file generated: {output_path}")
|
88
|
-
return output_path
|
48
|
+
self.config = config
|
49
|
+
self.logger = logger or logging.getLogger(__name__)
|
89
50
|
|
90
|
-
|
91
|
-
self.logger.error(f"Failed to generate LRC file: {str(e)}")
|
92
|
-
raise
|
51
|
+
self.logger.debug(f"Initializing OutputGenerator with config: {self.config}")
|
93
52
|
|
94
|
-
|
95
|
-
|
96
|
-
|
53
|
+
# Set video resolution parameters
|
54
|
+
self.video_resolution_num, self.font_size, self.line_height = self._get_video_params(self.config.video_resolution)
|
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}")
|
97
95
|
|
98
|
-
|
96
|
+
def generate_outputs(
|
97
|
+
self,
|
98
|
+
transcription_corrected: CorrectionResult,
|
99
|
+
lyrics_results: List[LyricsData],
|
100
|
+
output_prefix: str,
|
101
|
+
audio_filepath: str,
|
102
|
+
artist: Optional[str] = None,
|
103
|
+
title: Optional[str] = None,
|
104
|
+
) -> OutputPaths:
|
105
|
+
"""Generate all requested output formats."""
|
106
|
+
outputs = OutputPaths()
|
99
107
|
|
100
108
|
try:
|
101
|
-
|
102
|
-
|
103
|
-
|
109
|
+
# Generate plain lyrics files for each provider
|
110
|
+
for lyrics_data in lyrics_results:
|
111
|
+
self.plain_text.write_lyrics(lyrics_data, output_prefix)
|
112
|
+
|
113
|
+
# Write original (uncorrected) transcription
|
114
|
+
outputs.original_txt = self.plain_text.write_original_transcription(transcription_corrected, output_prefix)
|
115
|
+
|
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)
|
120
|
+
|
121
|
+
# Write corrected lyrics as plain text
|
122
|
+
outputs.corrected_txt = self.plain_text.write_corrected_lyrics(resized_segments, output_prefix)
|
123
|
+
|
124
|
+
# Generate LRC using LyricsFileGenerator
|
125
|
+
outputs.lrc = self.lyrics_file.generate_lrc(resized_segments, output_prefix)
|
126
|
+
|
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
|
+
)
|
104
136
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
f.write(line)
|
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)
|
111
142
|
|
112
|
-
|
113
|
-
return output_path
|
143
|
+
return outputs
|
114
144
|
|
115
145
|
except Exception as e:
|
116
|
-
self.logger.error(f"Failed to generate
|
146
|
+
self.logger.error(f"Failed to generate outputs: {str(e)}")
|
117
147
|
raise
|
118
148
|
|
119
|
-
def
|
120
|
-
"""Generate
|
121
|
-
self.
|
122
|
-
|
123
|
-
output_path = os.path.join(self.output_dir or self.cache_dir, f"{output_prefix}.mp4")
|
124
|
-
width, height = self.video_resolution_num
|
125
|
-
|
126
|
-
try:
|
127
|
-
# Prepare FFmpeg command
|
128
|
-
cmd = [
|
129
|
-
"ffmpeg",
|
130
|
-
"-y",
|
131
|
-
"-f",
|
132
|
-
"lavfi",
|
133
|
-
"-i",
|
134
|
-
f"color=c={self.video_background_color}:s={width}x{height}",
|
135
|
-
"-i",
|
136
|
-
audio_path,
|
137
|
-
"-vf",
|
138
|
-
f"ass={ass_path}",
|
139
|
-
"-c:v",
|
140
|
-
"libx264",
|
141
|
-
"-c:a",
|
142
|
-
"aac",
|
143
|
-
"-shortest",
|
144
|
-
output_path,
|
145
|
-
]
|
146
|
-
|
147
|
-
# If background image provided, use it instead of solid color
|
148
|
-
if self.video_background_image:
|
149
|
-
cmd[3:6] = ["-i", self.video_background_image]
|
150
|
-
|
151
|
-
self.logger.debug(f"Running FFmpeg command: {' '.join(cmd)}")
|
152
|
-
subprocess.run(cmd, check=True)
|
153
|
-
|
154
|
-
self.logger.info(f"Video generated: {output_path}")
|
155
|
-
return output_path
|
156
|
-
|
157
|
-
except subprocess.CalledProcessError as e:
|
158
|
-
self.logger.error(f"FFmpeg error: {str(e)}")
|
159
|
-
raise
|
160
|
-
except Exception as e:
|
161
|
-
self.logger.error(f"Failed to generate video: {str(e)}")
|
162
|
-
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}")
|
163
152
|
|
164
153
|
def _get_video_params(self, resolution: str) -> tuple:
|
165
|
-
"""Get video parameters based on resolution
|
154
|
+
"""Get video parameters: (width, height), font_size, line_height based on video resolution config."""
|
166
155
|
match resolution:
|
167
156
|
case "4k":
|
168
157
|
return (3840, 2160), 250, 250
|
@@ -171,40 +160,20 @@ class OutputGenerator:
|
|
171
160
|
case "720p":
|
172
161
|
return (1280, 720), 100, 100
|
173
162
|
case "360p":
|
174
|
-
return (640, 360),
|
163
|
+
return (640, 360), 40, 50
|
175
164
|
case _:
|
176
165
|
raise ValueError("Invalid video_resolution value. Must be one of: 4k, 1080p, 720p, 360p")
|
177
166
|
|
178
|
-
def
|
179
|
-
"""
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
centiseconds = int((seconds % 1) * 100)
|
192
|
-
seconds = int(seconds)
|
193
|
-
return f"{hours}:{minutes:02d}:{seconds:02d}.{centiseconds:02d}"
|
194
|
-
|
195
|
-
def _get_ass_header(self) -> str:
|
196
|
-
"""Get ASS format header with style definitions."""
|
197
|
-
width, height = self.video_resolution_num
|
198
|
-
return f"""[Script Info]
|
199
|
-
ScriptType: v4.00+
|
200
|
-
PlayResX: {width}
|
201
|
-
PlayResY: {height}
|
202
|
-
WrapStyle: 0
|
203
|
-
|
204
|
-
[V4+ Styles]
|
205
|
-
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
206
|
-
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
|
207
|
-
|
208
|
-
[Events]
|
209
|
-
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
210
|
-
"""
|
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")
|
171
|
+
|
172
|
+
try:
|
173
|
+
with open(output_path, "w", encoding="utf-8") as 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}")
|
176
|
+
return output_path
|
177
|
+
except Exception as e:
|
178
|
+
self.logger.error(f"Failed to write corrections data JSON: {str(e)}")
|
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
|