lyrics-transcriber 0.32.3__py3-none-any.whl → 0.34.0__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/cli/cli_main.py +19 -9
- lyrics_transcriber/core/config.py +14 -4
- lyrics_transcriber/core/controller.py +112 -27
- lyrics_transcriber/correction/corrector.py +9 -1
- lyrics_transcriber/lyrics/base_lyrics_provider.py +28 -7
- lyrics_transcriber/lyrics/genius.py +33 -6
- lyrics_transcriber/output/generator.py +32 -30
- lyrics_transcriber/output/segment_resizer.py +3 -2
- lyrics_transcriber/review/server.py +11 -5
- {lyrics_transcriber-0.32.3.dist-info → lyrics_transcriber-0.34.0.dist-info}/METADATA +1 -1
- {lyrics_transcriber-0.32.3.dist-info → lyrics_transcriber-0.34.0.dist-info}/RECORD +14 -14
- {lyrics_transcriber-0.32.3.dist-info → lyrics_transcriber-0.34.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.32.3.dist-info → lyrics_transcriber-0.34.0.dist-info}/WHEEL +0 -0
- {lyrics_transcriber-0.32.3.dist-info → lyrics_transcriber-0.34.0.dist-info}/entry_points.txt +0 -0
@@ -60,19 +60,24 @@ def create_arg_parser() -> argparse.ArgumentParser:
|
|
60
60
|
output_group.add_argument(
|
61
61
|
"--cache_dir",
|
62
62
|
type=Path,
|
63
|
-
help="Directory to cache downloaded/generated files. Default:
|
63
|
+
help="Directory to cache downloaded/generated files. Default: ~/lyrics-transcriber-cache/",
|
64
64
|
)
|
65
65
|
output_group.add_argument(
|
66
66
|
"--output_styles_json",
|
67
67
|
type=Path,
|
68
68
|
help="JSON file containing output style configurations for CDG and video generation",
|
69
69
|
)
|
70
|
-
output_group.add_argument("--generate_cdg", action="store_true", help="Generate CDG karaoke files")
|
71
70
|
|
72
|
-
#
|
73
|
-
|
74
|
-
|
75
|
-
|
71
|
+
# Feature control group
|
72
|
+
feature_group = parser.add_argument_group("Feature Control")
|
73
|
+
feature_group.add_argument("--skip_lyrics_fetch", action="store_true", help="Skip fetching lyrics from online sources")
|
74
|
+
feature_group.add_argument("--skip_transcription", action="store_true", help="Skip audio transcription process")
|
75
|
+
feature_group.add_argument("--skip_correction", action="store_true", help="Skip lyrics correction process")
|
76
|
+
feature_group.add_argument("--skip_plain_text", action="store_true", help="Skip generating plain text output files")
|
77
|
+
feature_group.add_argument("--skip_lrc", action="store_true", help="Skip generating LRC file")
|
78
|
+
feature_group.add_argument("--skip_cdg", action="store_true", help="Skip generating CDG karaoke files")
|
79
|
+
feature_group.add_argument("--skip_video", action="store_true", help="Skip rendering karaoke video")
|
80
|
+
feature_group.add_argument(
|
76
81
|
"--video_resolution", choices=["4k", "1080p", "720p", "360p"], default="360p", help="Resolution of the karaoke video. Default: 360p"
|
77
82
|
)
|
78
83
|
|
@@ -86,7 +91,7 @@ def parse_args(parser: argparse.ArgumentParser, args_list: list[str] | None = No
|
|
86
91
|
|
87
92
|
# Set default cache_dir if not provided
|
88
93
|
if not hasattr(args, "cache_dir") or args.cache_dir is None:
|
89
|
-
args.cache_dir = Path(os.getenv("LYRICS_TRANSCRIBER_CACHE_DIR", "
|
94
|
+
args.cache_dir = Path(os.getenv("LYRICS_TRANSCRIBER_CACHE_DIR", os.path.join(os.path.expanduser("~"), "lyrics-transcriber-cache")))
|
90
95
|
|
91
96
|
return args
|
92
97
|
|
@@ -135,9 +140,14 @@ def create_configs(args: argparse.Namespace, env_config: Dict[str, str]) -> tupl
|
|
135
140
|
output_styles_json=str(args.output_styles_json),
|
136
141
|
output_dir=str(args.output_dir) if args.output_dir else os.getcwd(),
|
137
142
|
cache_dir=str(args.cache_dir),
|
138
|
-
render_video=args.render_video,
|
139
|
-
generate_cdg=args.generate_cdg,
|
140
143
|
video_resolution=args.video_resolution,
|
144
|
+
fetch_lyrics=not args.skip_lyrics_fetch,
|
145
|
+
run_transcription=not args.skip_transcription,
|
146
|
+
run_correction=not args.skip_correction,
|
147
|
+
generate_plain_text=not args.skip_plain_text,
|
148
|
+
generate_lrc=not args.skip_lrc,
|
149
|
+
generate_cdg=not args.skip_cdg,
|
150
|
+
render_video=not args.skip_video,
|
141
151
|
)
|
142
152
|
|
143
153
|
return transcriber_config, lyrics_config, output_config
|
@@ -28,8 +28,18 @@ class OutputConfig:
|
|
28
28
|
max_line_length: int = 36
|
29
29
|
styles: Dict[str, Any] = field(default_factory=dict)
|
30
30
|
output_dir: Optional[str] = os.getcwd()
|
31
|
-
cache_dir: str = os.getenv(
|
32
|
-
|
33
|
-
|
34
|
-
|
31
|
+
cache_dir: str = os.getenv(
|
32
|
+
"LYRICS_TRANSCRIBER_CACHE_DIR",
|
33
|
+
os.path.join(os.path.expanduser("~"), "lyrics-transcriber-cache")
|
34
|
+
)
|
35
|
+
|
36
|
+
fetch_lyrics: bool = True
|
37
|
+
run_transcription: bool = True
|
38
|
+
run_correction: bool = True
|
35
39
|
enable_review: bool = True
|
40
|
+
|
41
|
+
generate_plain_text: bool = True
|
42
|
+
generate_lrc: bool = True
|
43
|
+
generate_cdg: bool = True
|
44
|
+
render_video: bool = True
|
45
|
+
video_resolution: str = "360p"
|
@@ -1,12 +1,9 @@
|
|
1
|
+
import difflib
|
1
2
|
import os
|
2
3
|
import logging
|
3
4
|
from dataclasses import dataclass, field
|
4
5
|
from typing import Dict, Optional, List
|
5
|
-
from lyrics_transcriber.types import
|
6
|
-
LyricsData,
|
7
|
-
TranscriptionResult,
|
8
|
-
CorrectionResult,
|
9
|
-
)
|
6
|
+
from lyrics_transcriber.types import LyricsData, TranscriptionResult, CorrectionResult
|
10
7
|
from lyrics_transcriber.transcribers.base_transcriber import BaseTranscriber
|
11
8
|
from lyrics_transcriber.transcribers.audioshake import AudioShakeTranscriber, AudioShakeConfig
|
12
9
|
from lyrics_transcriber.transcribers.whisper import WhisperTranscriber, WhisperConfig
|
@@ -83,6 +80,16 @@ class LyricsTranscriber:
|
|
83
80
|
self.lyrics_config = lyrics_config or LyricsConfig()
|
84
81
|
self.output_config = output_config or OutputConfig()
|
85
82
|
|
83
|
+
# Check if styles JSON is available for CDG and video features
|
84
|
+
if not self.output_config.output_styles_json or not os.path.exists(self.output_config.output_styles_json):
|
85
|
+
if self.output_config.generate_cdg or self.output_config.render_video:
|
86
|
+
self.logger.warning(
|
87
|
+
f"Output styles JSON file not found: {self.output_config.output_styles_json}. "
|
88
|
+
"CDG and video generation will be disabled."
|
89
|
+
)
|
90
|
+
self.output_config.generate_cdg = False
|
91
|
+
self.output_config.render_video = False
|
92
|
+
|
86
93
|
# Basic settings
|
87
94
|
self.audio_filepath = audio_filepath
|
88
95
|
self.artist = artist
|
@@ -106,6 +113,18 @@ class LyricsTranscriber:
|
|
106
113
|
self.corrector = corrector or LyricsCorrector(cache_dir=self.output_config.cache_dir, logger=self.logger)
|
107
114
|
self.output_generator = output_generator or self._initialize_output_generator()
|
108
115
|
|
116
|
+
# Log enabled features
|
117
|
+
self.logger.info("Enabled features:")
|
118
|
+
self.logger.info(f" Lyrics fetching: {'enabled' if self.output_config.fetch_lyrics else 'disabled'}")
|
119
|
+
self.logger.info(f" Transcription: {'enabled' if self.output_config.run_transcription else 'disabled'}")
|
120
|
+
self.logger.info(f" Lyrics correction: {'enabled' if self.output_config.run_correction else 'disabled'}")
|
121
|
+
self.logger.info(f" Plain text output: {'enabled' if self.output_config.generate_plain_text else 'disabled'}")
|
122
|
+
self.logger.info(f" LRC file generation: {'enabled' if self.output_config.generate_lrc else 'disabled'}")
|
123
|
+
self.logger.info(f" CDG file generation: {'enabled' if self.output_config.generate_cdg else 'disabled'}")
|
124
|
+
self.logger.info(f" Video rendering: {'enabled' if self.output_config.render_video else 'disabled'}")
|
125
|
+
if self.output_config.render_video:
|
126
|
+
self.logger.info(f" Video resolution: {self.output_config.video_resolution}")
|
127
|
+
|
109
128
|
def _initialize_transcribers(self) -> Dict[str, BaseTranscriber]:
|
110
129
|
"""Initialize available transcription services."""
|
111
130
|
transcribers = {}
|
@@ -175,27 +194,27 @@ class LyricsTranscriber:
|
|
175
194
|
return OutputGenerator(config=self.output_config, logger=self.logger)
|
176
195
|
|
177
196
|
def process(self) -> LyricsControllerResult:
|
178
|
-
"""
|
179
|
-
Main processing method that orchestrates the entire workflow.
|
180
|
-
|
181
|
-
Returns:
|
182
|
-
LyricsControllerResult containing all outputs and generated files.
|
197
|
+
"""Main processing method that orchestrates the entire workflow."""
|
183
198
|
|
184
|
-
|
185
|
-
|
186
|
-
"""
|
187
|
-
# Step 1: Fetch lyrics if artist and title are provided
|
188
|
-
if self.artist and self.title:
|
199
|
+
# Step 1: Fetch lyrics if enabled and artist/title are provided
|
200
|
+
if self.output_config.fetch_lyrics and self.artist and self.title:
|
189
201
|
self.fetch_lyrics()
|
190
202
|
|
191
|
-
# Step 2: Run transcription
|
192
|
-
self.
|
203
|
+
# Step 2: Run transcription if enabled
|
204
|
+
if self.output_config.run_transcription:
|
205
|
+
self.transcribe()
|
193
206
|
|
194
|
-
# Step 3: Process and correct lyrics
|
195
|
-
self.
|
207
|
+
# Step 3: Process and correct lyrics if enabled AND we have transcription results
|
208
|
+
if self.output_config.run_correction and self.results.transcription_results:
|
209
|
+
self.correct_lyrics()
|
210
|
+
elif self.output_config.run_correction:
|
211
|
+
self.logger.info("Skipping lyrics correction - no transcription results available")
|
196
212
|
|
197
|
-
# Step 4: Generate outputs
|
198
|
-
self.
|
213
|
+
# Step 4: Generate outputs based on what we have
|
214
|
+
if self.results.transcription_corrected or self.results.lyrics_results:
|
215
|
+
self.generate_outputs()
|
216
|
+
else:
|
217
|
+
self.logger.warning("No corrected transcription or lyrics available. Skipping output generation.")
|
199
218
|
|
200
219
|
self.logger.info("Processing completed successfully")
|
201
220
|
return self.results
|
@@ -239,7 +258,32 @@ class LyricsTranscriber:
|
|
239
258
|
"""Run lyrics correction using transcription and internet lyrics."""
|
240
259
|
self.logger.info("Starting lyrics correction process")
|
241
260
|
|
242
|
-
#
|
261
|
+
# Check if we have reference lyrics to work with
|
262
|
+
if not self.results.lyrics_results:
|
263
|
+
self.logger.warning("No reference lyrics available for correction - using raw transcription")
|
264
|
+
# Use the highest priority transcription result as the "corrected" version
|
265
|
+
if self.results.transcription_results:
|
266
|
+
sorted_results = sorted(self.results.transcription_results, key=lambda x: x.priority)
|
267
|
+
best_transcription = sorted_results[0]
|
268
|
+
|
269
|
+
# Create a CorrectionResult with no corrections
|
270
|
+
self.results.transcription_corrected = CorrectionResult(
|
271
|
+
original_segments=best_transcription.result.segments,
|
272
|
+
corrected_segments=best_transcription.result.segments,
|
273
|
+
corrected_text="", # Will be generated from segments
|
274
|
+
corrections=[], # No corrections made
|
275
|
+
corrections_made=0, # No corrections made
|
276
|
+
confidence=1.0, # Full confidence since we're using original
|
277
|
+
transcribed_text="", # Will be generated from segments
|
278
|
+
reference_texts={},
|
279
|
+
anchor_sequences=[],
|
280
|
+
gap_sequences=[],
|
281
|
+
resized_segments=[], # Will be populated later
|
282
|
+
metadata={"correction_type": "none", "reason": "no_reference_lyrics"},
|
283
|
+
)
|
284
|
+
return
|
285
|
+
|
286
|
+
# Run correction if we have reference lyrics
|
243
287
|
corrected_data = self.corrector.run(
|
244
288
|
transcription_results=self.results.transcription_results, lyrics_results=self.results.lyrics_results
|
245
289
|
)
|
@@ -249,19 +293,60 @@ class LyricsTranscriber:
|
|
249
293
|
self.logger.info("Lyrics correction completed")
|
250
294
|
|
251
295
|
# Add human review step
|
252
|
-
if self.output_config.enable_review:
|
296
|
+
if self.output_config.enable_review:
|
253
297
|
from ..review import start_review_server
|
298
|
+
import json
|
299
|
+
from copy import deepcopy
|
254
300
|
|
255
301
|
self.logger.info("Starting human review process")
|
256
|
-
|
302
|
+
|
303
|
+
def normalize_data(data_dict):
|
304
|
+
"""Normalize numeric values in the data structure before JSON conversion."""
|
305
|
+
if isinstance(data_dict, dict):
|
306
|
+
return {k: normalize_data(v) for k, v in data_dict.items()}
|
307
|
+
elif isinstance(data_dict, list):
|
308
|
+
return [normalize_data(item) for item in data_dict]
|
309
|
+
elif isinstance(data_dict, float):
|
310
|
+
# Convert whole number floats to integers
|
311
|
+
if data_dict.is_integer():
|
312
|
+
return int(data_dict)
|
313
|
+
return data_dict
|
314
|
+
return data_dict
|
315
|
+
|
316
|
+
# Normalize and convert auto-corrected data
|
317
|
+
auto_data = normalize_data(deepcopy(self.results.transcription_corrected.to_dict()))
|
318
|
+
auto_corrected_json = json.dumps(auto_data, indent=4).splitlines()
|
319
|
+
|
320
|
+
# Pass through review server
|
321
|
+
reviewed_data = start_review_server(self.results.transcription_corrected)
|
322
|
+
|
323
|
+
# Normalize and convert reviewed data
|
324
|
+
human_data = normalize_data(deepcopy(reviewed_data.to_dict()))
|
325
|
+
human_corrected_json = json.dumps(human_data, indent=4).splitlines()
|
326
|
+
|
257
327
|
self.logger.info("Human review completed")
|
258
328
|
|
329
|
+
# Compare the normalized JSON strings
|
330
|
+
diff = list(
|
331
|
+
difflib.unified_diff(auto_corrected_json, human_corrected_json, fromfile="auto-corrected", tofile="human-corrected")
|
332
|
+
)
|
333
|
+
|
334
|
+
if diff:
|
335
|
+
self.logger.warning("Changes made by human review:")
|
336
|
+
for line in diff:
|
337
|
+
self.logger.warning(line.rstrip())
|
338
|
+
|
339
|
+
# exit(1)
|
340
|
+
|
259
341
|
def generate_outputs(self) -> None:
|
260
|
-
"""Generate output files."""
|
342
|
+
"""Generate output files based on enabled features and available data."""
|
261
343
|
self.logger.info("Generating output files")
|
262
344
|
|
345
|
+
# Only proceed with outputs that make sense based on what we have
|
346
|
+
has_correction = bool(self.results.transcription_corrected)
|
347
|
+
|
263
348
|
output_files = self.output_generator.generate_outputs(
|
264
|
-
transcription_corrected=self.results.transcription_corrected,
|
349
|
+
transcription_corrected=self.results.transcription_corrected if has_correction else None,
|
265
350
|
lyrics_results=self.results.lyrics_results,
|
266
351
|
output_prefix=self.output_prefix,
|
267
352
|
audio_filepath=self.audio_filepath,
|
@@ -269,7 +354,7 @@ class LyricsTranscriber:
|
|
269
354
|
title=self.title,
|
270
355
|
)
|
271
356
|
|
272
|
-
# Store
|
357
|
+
# Store results
|
273
358
|
self.results.lrc_filepath = output_files.lrc
|
274
359
|
self.results.ass_filepath = output_files.ass
|
275
360
|
self.results.video_filepath = output_files.video
|
@@ -28,7 +28,8 @@ class LyricsCorrector:
|
|
28
28
|
logger: Optional[logging.Logger] = None,
|
29
29
|
):
|
30
30
|
self.logger = logger or logging.getLogger(__name__)
|
31
|
-
self.
|
31
|
+
self._anchor_finder = anchor_finder
|
32
|
+
self._cache_dir = cache_dir
|
32
33
|
|
33
34
|
# Default handlers in order of preference
|
34
35
|
self.handlers = handlers or [
|
@@ -42,6 +43,13 @@ class LyricsCorrector:
|
|
42
43
|
LevenshteinHandler(),
|
43
44
|
]
|
44
45
|
|
46
|
+
@property
|
47
|
+
def anchor_finder(self) -> AnchorSequenceFinder:
|
48
|
+
"""Lazy load the anchor finder instance, initializing it if not already set."""
|
49
|
+
if self._anchor_finder is None:
|
50
|
+
self._anchor_finder = AnchorSequenceFinder(cache_dir=self._cache_dir, logger=self.logger)
|
51
|
+
return self._anchor_finder
|
52
|
+
|
45
53
|
def run(self, transcription_results: List[TranscriptionResult], lyrics_results: List[LyricsData]) -> CorrectionResult:
|
46
54
|
"""Execute the correction process."""
|
47
55
|
if not transcription_results:
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
7
7
|
import os
|
8
8
|
from abc import ABC, abstractmethod
|
9
9
|
from lyrics_transcriber.types import LyricsData
|
10
|
+
from karaoke_lyrics_processor import KaraokeLyricsProcessor
|
10
11
|
|
11
12
|
|
12
13
|
@dataclass
|
@@ -17,6 +18,7 @@ class LyricsProviderConfig:
|
|
17
18
|
spotify_cookie: Optional[str] = None
|
18
19
|
cache_dir: Optional[str] = None
|
19
20
|
audio_filepath: Optional[str] = None
|
21
|
+
max_line_length: int = 36 # New config parameter for KaraokeLyricsProcessor
|
20
22
|
|
21
23
|
|
22
24
|
class BaseLyricsProvider(ABC):
|
@@ -26,6 +28,7 @@ class BaseLyricsProvider(ABC):
|
|
26
28
|
self.logger = logger or logging.getLogger(__name__)
|
27
29
|
self.cache_dir = Path(config.cache_dir) if config.cache_dir else None
|
28
30
|
self.audio_filepath = config.audio_filepath
|
31
|
+
self.max_line_length = config.max_line_length
|
29
32
|
if self.cache_dir:
|
30
33
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
31
34
|
self.logger.debug(f"Initialized {self.__class__.__name__} with cache dir: {self.cache_dir}")
|
@@ -35,21 +38,22 @@ class BaseLyricsProvider(ABC):
|
|
35
38
|
if not self.cache_dir:
|
36
39
|
return self._fetch_and_convert_result(artist, title)
|
37
40
|
|
38
|
-
|
39
|
-
|
41
|
+
# Use artist and title for cache key instead of audio file hash
|
42
|
+
cache_key = self._get_artist_title_hash(artist, title)
|
43
|
+
raw_cache_path = self._get_cache_path(cache_key, "raw")
|
40
44
|
|
41
45
|
# Try to load from cache first
|
42
46
|
raw_data = self._load_from_cache(raw_cache_path)
|
43
47
|
if raw_data is not None:
|
44
48
|
self.logger.info(f"Using cached lyrics for {artist} - {title}")
|
45
|
-
return self._save_and_convert_result(
|
49
|
+
return self._save_and_convert_result(cache_key, raw_data)
|
46
50
|
|
47
51
|
# If not in cache, fetch from source
|
48
52
|
raw_result = self._fetch_data_from_source(artist, title)
|
49
53
|
if raw_result:
|
50
54
|
# Save raw API response
|
51
55
|
self._save_to_cache(raw_cache_path, raw_result)
|
52
|
-
return self._save_and_convert_result(
|
56
|
+
return self._save_and_convert_result(cache_key, raw_result)
|
53
57
|
|
54
58
|
return None
|
55
59
|
|
@@ -95,13 +99,30 @@ class BaseLyricsProvider(ABC):
|
|
95
99
|
self.logger.warning(f"Cache file {cache_path} is corrupted")
|
96
100
|
return None
|
97
101
|
|
102
|
+
def _process_lyrics(self, lyrics_data: LyricsData) -> LyricsData:
|
103
|
+
"""Process lyrics using KaraokeLyricsProcessor."""
|
104
|
+
processor = KaraokeLyricsProcessor(
|
105
|
+
log_level=self.logger.getEffectiveLevel(),
|
106
|
+
log_formatter=self.logger.handlers[0].formatter if self.logger.handlers else None,
|
107
|
+
input_lyrics_text=lyrics_data.lyrics,
|
108
|
+
max_line_length=self.max_line_length,
|
109
|
+
)
|
110
|
+
processed_text = processor.process()
|
111
|
+
|
112
|
+
# Create new LyricsData with processed text
|
113
|
+
return LyricsData(source=lyrics_data.source, lyrics=processed_text, segments=lyrics_data.segments, metadata=lyrics_data.metadata)
|
114
|
+
|
98
115
|
def _save_and_convert_result(self, cache_key: str, raw_data: Dict[str, Any]) -> LyricsData:
|
99
|
-
"""Convert raw result to standardized format, save to cache, and return."""
|
116
|
+
"""Convert raw result to standardized format, process lyrics, save to cache, and return."""
|
100
117
|
converted_cache_path = self._get_cache_path(cache_key, "converted")
|
101
118
|
converted_result = self._convert_result_format(raw_data)
|
119
|
+
|
120
|
+
# Process the lyrics
|
121
|
+
processed_result = self._process_lyrics(converted_result)
|
122
|
+
|
102
123
|
# Convert to dictionary before saving to cache
|
103
|
-
self._save_to_cache(converted_cache_path,
|
104
|
-
return
|
124
|
+
self._save_to_cache(converted_cache_path, processed_result.to_dict())
|
125
|
+
return processed_result
|
105
126
|
|
106
127
|
def _fetch_and_convert_result(self, artist: str, title: str) -> Optional[LyricsData]:
|
107
128
|
"""Fetch and convert result when caching is disabled."""
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import logging
|
2
|
+
import re
|
2
3
|
from typing import Optional, Dict, Any
|
3
4
|
import lyricsgenius
|
4
5
|
from lyrics_transcriber.types import LyricsData, LyricsMetadata
|
@@ -13,9 +14,15 @@ class GeniusProvider(BaseLyricsProvider):
|
|
13
14
|
self.api_token = config.genius_api_token
|
14
15
|
self.client = None
|
15
16
|
if self.api_token:
|
16
|
-
self.client = lyricsgenius.Genius(
|
17
|
-
|
18
|
-
|
17
|
+
self.client = lyricsgenius.Genius(
|
18
|
+
self.api_token,
|
19
|
+
verbose=(logger.getEffectiveLevel() == logging.DEBUG if logger else False),
|
20
|
+
remove_section_headers=True, # Remove [Chorus], [Verse], etc.
|
21
|
+
skip_non_songs=True, # Skip track listings and other non-song results
|
22
|
+
timeout=10, # Reasonable timeout for requests
|
23
|
+
retries=3, # Number of retries for failed requests
|
24
|
+
sleep_time=1, # Small delay between requests to be nice to the API
|
25
|
+
)
|
19
26
|
|
20
27
|
def _fetch_data_from_source(self, artist: str, title: str) -> Optional[Dict[str, Any]]:
|
21
28
|
"""Fetch raw song data from Genius API."""
|
@@ -35,6 +42,9 @@ class GeniusProvider(BaseLyricsProvider):
|
|
35
42
|
|
36
43
|
def _convert_result_format(self, raw_data: Dict[str, Any]) -> LyricsData:
|
37
44
|
"""Convert Genius's raw API response to standardized format."""
|
45
|
+
# Clean the lyrics before processing
|
46
|
+
lyrics = self._clean_lyrics(raw_data.get("lyrics", ""))
|
47
|
+
|
38
48
|
# Extract release date components if available
|
39
49
|
release_date = None
|
40
50
|
if release_components := raw_data.get("release_date_components"):
|
@@ -68,6 +78,23 @@ class GeniusProvider(BaseLyricsProvider):
|
|
68
78
|
)
|
69
79
|
|
70
80
|
# Create result object
|
71
|
-
return LyricsData(
|
72
|
-
|
73
|
-
|
81
|
+
return LyricsData(source="genius", lyrics=lyrics, segments=[], metadata=metadata)
|
82
|
+
|
83
|
+
def _clean_lyrics(self, lyrics: str) -> str:
|
84
|
+
"""Clean and process lyrics from Genius to remove unwanted content."""
|
85
|
+
|
86
|
+
lyrics = lyrics.replace("\\n", "\n")
|
87
|
+
lyrics = re.sub(r"You might also like", "", lyrics)
|
88
|
+
lyrics = re.sub(
|
89
|
+
r".*?Lyrics([A-Z])", r"\1", lyrics
|
90
|
+
) # Remove the song name and word "Lyrics" if this has a non-newline char at the start
|
91
|
+
lyrics = re.sub(r"^[0-9]* Contributors.*Lyrics", "", lyrics) # Remove this example: 27 ContributorsSex Bomb Lyrics
|
92
|
+
lyrics = re.sub(
|
93
|
+
r"See.*Live.*Get tickets as low as \$[0-9]+", "", lyrics
|
94
|
+
) # Remove this example: See Tom Jones LiveGet tickets as low as $71
|
95
|
+
lyrics = re.sub(r"[0-9]+Embed$", "", lyrics) # Remove the word "Embed" at end of line with preceding numbers if found
|
96
|
+
lyrics = re.sub(r"(\S)Embed$", r"\1", lyrics) # Remove the word "Embed" if it has been tacked onto a word at the end of a line
|
97
|
+
lyrics = re.sub(r"^Embed$", r"", lyrics) # Remove the word "Embed" if it has been tacked onto a word at the end of a line
|
98
|
+
lyrics = re.sub(r".*?\[.*?\].*?", "", lyrics) # Remove lines containing square brackets
|
99
|
+
# add any additional cleaning rules here
|
100
|
+
return lyrics
|
@@ -95,7 +95,7 @@ class OutputGenerator:
|
|
95
95
|
|
96
96
|
def generate_outputs(
|
97
97
|
self,
|
98
|
-
transcription_corrected: CorrectionResult,
|
98
|
+
transcription_corrected: Optional[CorrectionResult],
|
99
99
|
lyrics_results: List[LyricsData],
|
100
100
|
output_prefix: str,
|
101
101
|
audio_filepath: str,
|
@@ -110,35 +110,37 @@ class OutputGenerator:
|
|
110
110
|
for lyrics_data in lyrics_results:
|
111
111
|
self.plain_text.write_lyrics(lyrics_data, output_prefix)
|
112
112
|
|
113
|
-
#
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
# Generate
|
140
|
-
|
141
|
-
|
113
|
+
# Only process transcription-related outputs if we have transcription data
|
114
|
+
if transcription_corrected:
|
115
|
+
# Write original (uncorrected) transcription
|
116
|
+
outputs.original_txt = self.plain_text.write_original_transcription(transcription_corrected, output_prefix)
|
117
|
+
|
118
|
+
# Resize corrected segments to ensure none are longer than max_line_length
|
119
|
+
resized_segments = self.segment_resizer.resize_segments(transcription_corrected.corrected_segments)
|
120
|
+
transcription_corrected.resized_segments = resized_segments
|
121
|
+
outputs.corrections_json = self.write_corrections_data(transcription_corrected, output_prefix)
|
122
|
+
|
123
|
+
# Write corrected lyrics as plain text
|
124
|
+
outputs.corrected_txt = self.plain_text.write_corrected_lyrics(resized_segments, output_prefix)
|
125
|
+
|
126
|
+
# Generate LRC using LyricsFileGenerator
|
127
|
+
outputs.lrc = self.lyrics_file.generate_lrc(resized_segments, output_prefix)
|
128
|
+
|
129
|
+
# Generate CDG file if requested
|
130
|
+
if self.config.generate_cdg:
|
131
|
+
outputs.cdg, outputs.mp3, outputs.cdg_zip = self.cdg.generate_cdg(
|
132
|
+
segments=resized_segments,
|
133
|
+
audio_file=audio_filepath,
|
134
|
+
title=title or output_prefix,
|
135
|
+
artist=artist or "",
|
136
|
+
cdg_styles=self.config.styles["cdg"],
|
137
|
+
)
|
138
|
+
|
139
|
+
# Generate video if requested
|
140
|
+
if self.config.render_video:
|
141
|
+
# Generate ASS subtitles
|
142
|
+
outputs.ass = self.subtitle.generate_ass(resized_segments, output_prefix, audio_filepath)
|
143
|
+
outputs.video = self.video.generate_video(outputs.ass, audio_filepath, output_prefix)
|
142
144
|
|
143
145
|
return outputs
|
144
146
|
|
@@ -123,8 +123,9 @@ class SegmentResizer:
|
|
123
123
|
LyricsSegment(text="Here's another one.", ...)
|
124
124
|
]
|
125
125
|
"""
|
126
|
-
self.logger.info(f"Processing oversized segment {segment_idx}: '{segment.text}'")
|
127
126
|
segment_text = self._clean_text(segment.text)
|
127
|
+
|
128
|
+
self.logger.info(f"Processing oversized segment {segment_idx}: '{segment_text}'")
|
128
129
|
split_lines = self._process_segment_text(segment_text)
|
129
130
|
self.logger.debug(f"Split into {len(split_lines)} lines: {split_lines}")
|
130
131
|
|
@@ -163,7 +164,7 @@ class SegmentResizer:
|
|
163
164
|
if word_pos != -1:
|
164
165
|
line_words.append(words_to_process.pop(0))
|
165
166
|
# Remove the word and any following spaces from remaining line
|
166
|
-
remaining_line = remaining_line[word_pos + len(word_clean):].strip()
|
167
|
+
remaining_line = remaining_line[word_pos + len(word_clean) :].strip()
|
167
168
|
continue
|
168
169
|
|
169
170
|
# If we can't find the word in the remaining line, we're done with this line
|
@@ -2,7 +2,7 @@ import logging
|
|
2
2
|
from fastapi import FastAPI, Body
|
3
3
|
from fastapi.middleware.cors import CORSMiddleware
|
4
4
|
from typing import Optional, Dict, Any
|
5
|
-
from ..types import CorrectionResult
|
5
|
+
from ..types import CorrectionResult, WordCorrection, LyricsSegment
|
6
6
|
import time
|
7
7
|
import subprocess
|
8
8
|
import os
|
@@ -64,16 +64,22 @@ async def complete_review(updated_data: Dict[str, Any] = Body(...)):
|
|
64
64
|
Mark the review as complete and update the correction data.
|
65
65
|
|
66
66
|
Args:
|
67
|
-
updated_data:
|
67
|
+
updated_data: Dictionary containing corrections and corrected_segments
|
68
68
|
"""
|
69
69
|
global review_completed, current_review
|
70
70
|
|
71
71
|
logger.info("Received updated correction data")
|
72
72
|
|
73
73
|
try:
|
74
|
-
#
|
75
|
-
|
76
|
-
|
74
|
+
# Only update the specific fields that were modified
|
75
|
+
if current_review is None:
|
76
|
+
raise ValueError("No review in progress")
|
77
|
+
|
78
|
+
# Update only the corrections and corrected_segments
|
79
|
+
current_review.corrections = [WordCorrection.from_dict(c) for c in updated_data["corrections"]]
|
80
|
+
current_review.corrected_segments = [LyricsSegment.from_dict(s) for s in updated_data["corrected_segments"]]
|
81
|
+
current_review.corrections_made = len(current_review.corrections)
|
82
|
+
|
77
83
|
logger.info(f"Successfully updated correction data with {len(current_review.corrections)} corrections")
|
78
84
|
|
79
85
|
review_completed = True
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: lyrics-transcriber
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.34.0
|
4
4
|
Summary: Automatically create synchronised lyrics files in ASS and MidiCo LRC formats with word-level timestamps, using Whisper and lyrics from Genius and Spotify
|
5
5
|
License: MIT
|
6
6
|
Author: Andrew Beveridge
|
@@ -1,11 +1,11 @@
|
|
1
1
|
lyrics_transcriber/__init__.py,sha256=JpdjDK1MH_Be2XiSQWnb4i5Bbil1uPMA_KcuDZ3cyUI,240
|
2
2
|
lyrics_transcriber/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
lyrics_transcriber/cli/cli_main.py,sha256=
|
3
|
+
lyrics_transcriber/cli/cli_main.py,sha256=TFB7CwzgLuwPfoV7ggPPe5dh4WKNcWRoZkCu_WWUcLQ,9818
|
4
4
|
lyrics_transcriber/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
-
lyrics_transcriber/core/config.py,sha256=
|
6
|
-
lyrics_transcriber/core/controller.py,sha256=
|
5
|
+
lyrics_transcriber/core/config.py,sha256=y6MsAL0gFz7zRErtRRF81Z0vFOrySIrCw2aKDHExBz8,1160
|
6
|
+
lyrics_transcriber/core/controller.py,sha256=o3nGoNWFGbeXAKtqbWFArede1UNmCip8U1bn8viVlwo,17493
|
7
7
|
lyrics_transcriber/correction/anchor_sequence.py,sha256=YpKyY24Va5i4JgzP9ssqlOIkaYu060KaldiehbfgTdk,22200
|
8
|
-
lyrics_transcriber/correction/corrector.py,sha256=
|
8
|
+
lyrics_transcriber/correction/corrector.py,sha256=SFEmueWtTUipztVDaV8yTDsKp8XMHBZcZ343Z5NHSLE,13303
|
9
9
|
lyrics_transcriber/correction/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
10
|
lyrics_transcriber/correction/handlers/base.py,sha256=Vanmp6ykP5cdejuJ5ttzjP0Wl4JgKBL-mHbo9EFaeVc,1009
|
11
11
|
lyrics_transcriber/correction/handlers/extend_anchor.py,sha256=9rBrZPmc4grMSnCL2ilkcBsWHc05s6RBL9GDyNAplJk,3821
|
@@ -19,8 +19,8 @@ lyrics_transcriber/correction/handlers/word_count_match.py,sha256=zbyZ01VE_6azaF
|
|
19
19
|
lyrics_transcriber/correction/handlers/word_operations.py,sha256=2COTaJsEwpSWyXHXmGgjfcf2x7tbAnsQ0dIW0qyHYK4,5141
|
20
20
|
lyrics_transcriber/correction/phrase_analyzer.py,sha256=dtO_2LjxnPdHJM7De40mYIdHCkozwhizVVQp5XGO7x0,16962
|
21
21
|
lyrics_transcriber/correction/text_utils.py,sha256=VkOqgZHa9wEqLJdVNi4-KLFojQ6d4lWOGl_Y_vknenU,808
|
22
|
-
lyrics_transcriber/lyrics/base_lyrics_provider.py,sha256=
|
23
|
-
lyrics_transcriber/lyrics/genius.py,sha256=
|
22
|
+
lyrics_transcriber/lyrics/base_lyrics_provider.py,sha256=l61XJCvazt7wb6_vIQ23N8x9Otane8Pac5nvnBVCig8,6563
|
23
|
+
lyrics_transcriber/lyrics/genius.py,sha256=x8dNOygrDRZgwK0v2qK6F6wmqGEIiXe_Edgx-IkNWHA,5003
|
24
24
|
lyrics_transcriber/lyrics/spotify.py,sha256=9n4n98xS_BrpTPZg-24n0mzyPk9vkdmhy6T8ei8imh4,3599
|
25
25
|
lyrics_transcriber/output/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
26
26
|
lyrics_transcriber/output/ass/__init__.py,sha256=EYQ45gI7_-vclVgzISL0ML8VgxCdB0odqEyPyiPCIw0,578
|
@@ -65,22 +65,22 @@ lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf,sha256=WNG5LOQ-uGUF_WWT5aQ
|
|
65
65
|
lyrics_transcriber/output/fonts/arial.ttf,sha256=NcDzVZ2NtWnjbDEJW4pg1EFkPZX1kTneQOI_ragZuDM,275572
|
66
66
|
lyrics_transcriber/output/fonts/georgia.ttf,sha256=fQuyDGMrtZ6BoIhfVzvSFz9x9zIE3pBY_raM4DIicHI,142964
|
67
67
|
lyrics_transcriber/output/fonts/verdana.ttf,sha256=lu0UlJyktzks_yNbnEHVXBJTgqu-DA08K53WaJfK4Ms,139640
|
68
|
-
lyrics_transcriber/output/generator.py,sha256=
|
68
|
+
lyrics_transcriber/output/generator.py,sha256=W_wUo3Plt0A_H48WGbti4NeiE6eZAW-iRLwDnEOPkts,7715
|
69
69
|
lyrics_transcriber/output/lyrics_file.py,sha256=_KQyQjCOMIwQdQ0115uEAUIjQWTRmShkSfQuINPKxaw,3741
|
70
70
|
lyrics_transcriber/output/plain_text.py,sha256=3mYKq0BLYz1rGBD6ROjG2dn6BPuzbn5dxIQbWZVi4ao,3689
|
71
|
-
lyrics_transcriber/output/segment_resizer.py,sha256=
|
71
|
+
lyrics_transcriber/output/segment_resizer.py,sha256=b553FCdcjYAl9T1IA5K6ya0pcn1-irD5spmxSc26wnI,17143
|
72
72
|
lyrics_transcriber/output/subtitles.py,sha256=BQy7N_2zdBBWEiHL0NWFz3ZgAerWqQvTLALgxxK3Etk,16920
|
73
73
|
lyrics_transcriber/output/video.py,sha256=kYGeEMYtoJvrGnMuyNpuSmu2DTskGDXBNlrv6ddvC8I,8485
|
74
74
|
lyrics_transcriber/review/__init__.py,sha256=_3Eqw-uXZhOZwo6_sHZLhP9vxAVkLF9EBXduUvPdLjQ,57
|
75
|
-
lyrics_transcriber/review/server.py,sha256=
|
75
|
+
lyrics_transcriber/review/server.py,sha256=xUW55PhAeCKldXFm6F2X7waYid5vI_BsiPSoF4KnO0g,4744
|
76
76
|
lyrics_transcriber/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
77
77
|
lyrics_transcriber/storage/dropbox.py,sha256=Dyam1ULTkoxD1X5trkZ5dGp5XhBGCn998moC8IS9-68,9804
|
78
78
|
lyrics_transcriber/transcribers/audioshake.py,sha256=QzKGimVa6BovlvYFj35CbGpaGePI_DApAJGEBR_JQLc,8709
|
79
79
|
lyrics_transcriber/transcribers/base_transcriber.py,sha256=yPzUWPTCGmzE97H5Rz6g61e-qEGL77ZzUoiBOmswhts,5973
|
80
80
|
lyrics_transcriber/transcribers/whisper.py,sha256=P0kas2_oX16MO1-Qy7U5gl5KQN-RuUIJZz7LsEFLUiE,12906
|
81
81
|
lyrics_transcriber/types.py,sha256=xGf3hkTRcGZTTAjMVIev2i2DOU6co0QGpW8NxvaBQAA,16759
|
82
|
-
lyrics_transcriber-0.
|
83
|
-
lyrics_transcriber-0.
|
84
|
-
lyrics_transcriber-0.
|
85
|
-
lyrics_transcriber-0.
|
86
|
-
lyrics_transcriber-0.
|
82
|
+
lyrics_transcriber-0.34.0.dist-info/LICENSE,sha256=BiPihPDxhxIPEx6yAxVfAljD5Bhm_XG2teCbPEj_m0Y,1069
|
83
|
+
lyrics_transcriber-0.34.0.dist-info/METADATA,sha256=-NmP0C2ecou2bru2kFgDVCchjN10D-QdZsigcETfCFM,5856
|
84
|
+
lyrics_transcriber-0.34.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
85
|
+
lyrics_transcriber-0.34.0.dist-info/entry_points.txt,sha256=ChnmR13YoalGnC3sHW0TppX5FbhEXntYIha24tVQJ1M,104
|
86
|
+
lyrics_transcriber-0.34.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
{lyrics_transcriber-0.32.3.dist-info → lyrics_transcriber-0.34.0.dist-info}/entry_points.txt
RENAMED
File without changes
|