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.
@@ -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: /tmp/lyrics-transcriber-cache/",
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
- # Video options
73
- video_group = parser.add_argument_group("Video Options")
74
- video_group.add_argument("--render_video", action="store_true", help="Render a karaoke video with the generated lyrics")
75
- video_group.add_argument(
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", "/tmp/lyrics-transcriber-cache/"))
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("LYRICS_TRANSCRIBER_CACHE_DIR", "/tmp/lyrics-transcriber-cache/")
32
- render_video: bool = False
33
- generate_cdg: bool = False
34
- video_resolution: str = "360p"
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
- Raises:
185
- Exception: If a critical error occurs during processing.
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.transcribe()
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.correct_lyrics()
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.generate_outputs()
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
- # Run correction
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: # We'll need to add this config option
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
- self.results.transcription_corrected = start_review_server(corrected_data)
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 all output paths in results
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.anchor_finder = anchor_finder or AnchorSequenceFinder(cache_dir=cache_dir, logger=self.logger)
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
- file_hash = self._get_file_hash(self.audio_filepath)
39
- raw_cache_path = self._get_cache_path(file_hash, "raw")
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(file_hash, raw_data)
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(file_hash, raw_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, converted_result.to_dict())
104
- return converted_result
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(self.api_token)
17
- self.client.verbose = False
18
- self.client.remove_section_headers = True
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
- source="genius", lyrics=raw_data.get("lyrics", ""), segments=[], metadata=metadata
73
- ) # Genius doesn't provide timestamp data
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
- # 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
- )
136
-
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)
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: The complete correction result data with any modifications
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
- # Update the current review with modified data
75
- # We use from_dict to ensure the data is properly structured
76
- current_review = CorrectionResult.from_dict(updated_data)
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.32.3
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=pquvPfhw6brNgUyMuAzXfCUXNN0NM5tP_MyxlLWqNPc,8968
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=eddocVn_VoBGRqgBzfSvO7EZp3NuoGfSmd6mcT8wa74,941
6
- lyrics_transcriber/core/controller.py,sha256=eQ0M67SWIA-hr23fkw6F8hmqKkklOHsxOsJnYQLXFBE,12184
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=VOM6YhbANu00rYs6JpKHGZXnZtD5fxArnYtRrsp1YM4,12998
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=i4wxzu8nk2a3NDtnB_4r6rOGBZ7WvJFVlcEBjAkUYgI,5511
23
- lyrics_transcriber/lyrics/genius.py,sha256=M4rs3yk5RKW-RYfMm9w-UxwKQ8itgYeM-kVS6LCn8D0,3295
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=HQa3Ft8SKJie9-cYO0NKDbAU2-h_YnnH5wACxj0qFKw,7482
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=xkKCNt4CTdTErUTYsYtjmllKY8YHny1srqQMrJQYbK8,17141
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=iAG0WUkGrqnAF7dI4ZQQayp2qaamqGGYT6rWJF9OysI,4397
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.32.3.dist-info/LICENSE,sha256=BiPihPDxhxIPEx6yAxVfAljD5Bhm_XG2teCbPEj_m0Y,1069
83
- lyrics_transcriber-0.32.3.dist-info/METADATA,sha256=gKyuaWObELiKS3aopmqaSo-mvaA4-Via4Q8vza819zs,5856
84
- lyrics_transcriber-0.32.3.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
85
- lyrics_transcriber-0.32.3.dist-info/entry_points.txt,sha256=ChnmR13YoalGnC3sHW0TppX5FbhEXntYIha24tVQJ1M,104
86
- lyrics_transcriber-0.32.3.dist-info/RECORD,,
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,,