lyrics-transcriber 0.56.1__py3-none-any.whl → 0.57.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.
@@ -0,0 +1,352 @@
1
+ """
2
+ Correction Operations Module
3
+
4
+ This module contains reusable correction operations that can be shared between
5
+ the local ReviewServer and remote Modal serverless implementations.
6
+
7
+ These operations handle dynamic updates to correction results including:
8
+ - Adding new lyrics sources
9
+ - Updating correction handlers
10
+ - Generating preview videos
11
+ - Updating correction data
12
+ """
13
+
14
+ import json
15
+ import hashlib
16
+ import logging
17
+ from typing import Dict, Any, List, Optional
18
+ from pathlib import Path
19
+
20
+ from lyrics_transcriber.types import (
21
+ CorrectionResult,
22
+ WordCorrection,
23
+ LyricsSegment,
24
+ Word,
25
+ TranscriptionResult,
26
+ TranscriptionData,
27
+ LyricsData
28
+ )
29
+ from lyrics_transcriber.correction.corrector import LyricsCorrector
30
+ from lyrics_transcriber.lyrics.user_input_provider import UserInputProvider
31
+ from lyrics_transcriber.output.generator import OutputGenerator
32
+ from lyrics_transcriber.core.config import OutputConfig
33
+
34
+
35
+ class CorrectionOperations:
36
+ """Static methods for common correction operations."""
37
+
38
+ @staticmethod
39
+ def update_correction_result_with_data(
40
+ base_result: CorrectionResult,
41
+ updated_data: Dict[str, Any]
42
+ ) -> CorrectionResult:
43
+ """Update a CorrectionResult with new correction data from dict."""
44
+ return CorrectionResult(
45
+ corrections=[
46
+ WordCorrection(
47
+ original_word=c.get("original_word", "").strip(),
48
+ corrected_word=c.get("corrected_word", "").strip(),
49
+ original_position=c.get("original_position", 0),
50
+ source=c.get("source", "review"),
51
+ reason=c.get("reason", "manual_review"),
52
+ segment_index=c.get("segment_index", 0),
53
+ confidence=c.get("confidence"),
54
+ alternatives=c.get("alternatives", {}),
55
+ is_deletion=c.get("is_deletion", False),
56
+ split_index=c.get("split_index"),
57
+ split_total=c.get("split_total"),
58
+ corrected_position=c.get("corrected_position"),
59
+ reference_positions=c.get("reference_positions"),
60
+ length=c.get("length", 1),
61
+ handler=c.get("handler"),
62
+ word_id=c.get("word_id"),
63
+ corrected_word_id=c.get("corrected_word_id"),
64
+ )
65
+ for c in updated_data["corrections"]
66
+ ],
67
+ corrected_segments=[
68
+ LyricsSegment(
69
+ id=s["id"],
70
+ text=s["text"].strip(),
71
+ words=[
72
+ Word(
73
+ id=w["id"],
74
+ text=w["text"].strip(),
75
+ start_time=w["start_time"],
76
+ end_time=w["end_time"],
77
+ confidence=w.get("confidence"),
78
+ created_during_correction=w.get("created_during_correction", False),
79
+ )
80
+ for w in s["words"]
81
+ ],
82
+ start_time=s["start_time"],
83
+ end_time=s["end_time"],
84
+ )
85
+ for s in updated_data["corrected_segments"]
86
+ ],
87
+ # Copy existing fields from the base result
88
+ original_segments=base_result.original_segments,
89
+ corrections_made=len(updated_data["corrections"]),
90
+ confidence=base_result.confidence,
91
+ reference_lyrics=base_result.reference_lyrics,
92
+ anchor_sequences=base_result.anchor_sequences,
93
+ gap_sequences=base_result.gap_sequences,
94
+ resized_segments=None, # Will be generated if needed
95
+ metadata=base_result.metadata,
96
+ correction_steps=base_result.correction_steps,
97
+ word_id_map=base_result.word_id_map,
98
+ segment_id_map=base_result.segment_id_map,
99
+ )
100
+
101
+ @staticmethod
102
+ def add_lyrics_source(
103
+ correction_result: CorrectionResult,
104
+ source: str,
105
+ lyrics_text: str,
106
+ cache_dir: str,
107
+ logger: Optional[logging.Logger] = None
108
+ ) -> CorrectionResult:
109
+ """
110
+ Add a new lyrics source and rerun correction.
111
+
112
+ Args:
113
+ correction_result: Current correction result
114
+ source: Name of the new lyrics source
115
+ lyrics_text: The lyrics text content
116
+ cache_dir: Cache directory for correction operations
117
+ logger: Optional logger instance
118
+
119
+ Returns:
120
+ Updated CorrectionResult with new lyrics source and corrections
121
+
122
+ Raises:
123
+ ValueError: If source name is already in use or inputs are invalid
124
+ """
125
+ if not logger:
126
+ logger = logging.getLogger(__name__)
127
+
128
+ logger.info(f"Adding lyrics source '{source}' with {len(lyrics_text)} characters")
129
+
130
+ # Validate inputs
131
+ if not source or not lyrics_text:
132
+ raise ValueError("Source name and lyrics text are required")
133
+
134
+ if source in correction_result.reference_lyrics:
135
+ raise ValueError(f"Source name '{source}' is already in use")
136
+
137
+ # Store existing audio hash
138
+ audio_hash = correction_result.metadata.get("audio_hash") if correction_result.metadata else None
139
+
140
+ # Create lyrics data using the provider
141
+ logger.info("Creating LyricsData using UserInputProvider")
142
+ provider = UserInputProvider(
143
+ lyrics_text=lyrics_text,
144
+ source_name=source,
145
+ metadata=correction_result.metadata or {},
146
+ logger=logger
147
+ )
148
+ lyrics_data = provider._convert_result_format({
149
+ "text": lyrics_text,
150
+ "metadata": correction_result.metadata or {}
151
+ })
152
+ logger.info(f"Created LyricsData with {len(lyrics_data.segments)} segments")
153
+
154
+ # Add to reference lyrics (create a copy to avoid modifying original)
155
+ updated_reference_lyrics = correction_result.reference_lyrics.copy()
156
+ updated_reference_lyrics[source] = lyrics_data
157
+ logger.info(f"Added source '{source}' to reference lyrics")
158
+
159
+ # Create TranscriptionData from original segments
160
+ transcription_data = TranscriptionData(
161
+ segments=correction_result.original_segments,
162
+ words=[word for segment in correction_result.original_segments for word in segment.words],
163
+ text="\n".join(segment.text for segment in correction_result.original_segments),
164
+ source="original",
165
+ )
166
+
167
+ # Get currently enabled handlers from metadata
168
+ enabled_handlers = None
169
+ if correction_result.metadata:
170
+ enabled_handlers = correction_result.metadata.get("enabled_handlers")
171
+
172
+ # Rerun correction with updated reference lyrics
173
+ logger.info("Running correction with updated reference lyrics")
174
+ corrector = LyricsCorrector(
175
+ cache_dir=cache_dir,
176
+ enabled_handlers=enabled_handlers,
177
+ logger=logger,
178
+ )
179
+
180
+ updated_result = corrector.run(
181
+ transcription_results=[TranscriptionResult(name="original", priority=1, result=transcription_data)],
182
+ lyrics_results=updated_reference_lyrics,
183
+ metadata=correction_result.metadata,
184
+ )
185
+
186
+ # Update metadata with handler state
187
+ if not updated_result.metadata:
188
+ updated_result.metadata = {}
189
+ updated_result.metadata.update({
190
+ "available_handlers": corrector.all_handlers,
191
+ "enabled_handlers": [getattr(handler, "name", handler.__class__.__name__) for handler in corrector.handlers],
192
+ })
193
+
194
+ # Restore audio hash
195
+ if audio_hash:
196
+ updated_result.metadata["audio_hash"] = audio_hash
197
+
198
+ logger.info(f"Successfully added lyrics source '{source}' and updated corrections")
199
+ return updated_result
200
+
201
+ @staticmethod
202
+ def update_correction_handlers(
203
+ correction_result: CorrectionResult,
204
+ enabled_handlers: List[str],
205
+ cache_dir: str,
206
+ logger: Optional[logging.Logger] = None
207
+ ) -> CorrectionResult:
208
+ """
209
+ Update enabled correction handlers and rerun correction.
210
+
211
+ Args:
212
+ correction_result: Current correction result
213
+ enabled_handlers: List of handler names to enable
214
+ cache_dir: Cache directory for correction operations
215
+ logger: Optional logger instance
216
+
217
+ Returns:
218
+ Updated CorrectionResult with new handler configuration
219
+ """
220
+ if not logger:
221
+ logger = logging.getLogger(__name__)
222
+
223
+ logger.info(f"Updating correction handlers: {enabled_handlers}")
224
+
225
+ # Store existing audio hash
226
+ audio_hash = correction_result.metadata.get("audio_hash") if correction_result.metadata else None
227
+
228
+ # Update metadata with new handler configuration
229
+ updated_metadata = (correction_result.metadata or {}).copy()
230
+ updated_metadata["enabled_handlers"] = enabled_handlers
231
+
232
+ # Create TranscriptionData from original segments
233
+ transcription_data = TranscriptionData(
234
+ segments=correction_result.original_segments,
235
+ words=[word for segment in correction_result.original_segments for word in segment.words],
236
+ text="\n".join(segment.text for segment in correction_result.original_segments),
237
+ source="original",
238
+ )
239
+
240
+ # Rerun correction with updated handlers
241
+ logger.info("Running correction with updated handlers")
242
+ corrector = LyricsCorrector(
243
+ cache_dir=cache_dir,
244
+ enabled_handlers=enabled_handlers,
245
+ logger=logger,
246
+ )
247
+
248
+ updated_result = corrector.run(
249
+ transcription_results=[TranscriptionResult(name="original", priority=1, result=transcription_data)],
250
+ lyrics_results=correction_result.reference_lyrics,
251
+ metadata=updated_metadata,
252
+ )
253
+
254
+ # Update metadata with handler state
255
+ if not updated_result.metadata:
256
+ updated_result.metadata = {}
257
+ updated_result.metadata.update({
258
+ "available_handlers": corrector.all_handlers,
259
+ "enabled_handlers": [getattr(handler, "name", handler.__class__.__name__) for handler in corrector.handlers],
260
+ })
261
+
262
+ # Restore audio hash
263
+ if audio_hash:
264
+ updated_result.metadata["audio_hash"] = audio_hash
265
+
266
+ logger.info(f"Successfully updated handlers: {enabled_handlers}")
267
+ return updated_result
268
+
269
+ @staticmethod
270
+ def generate_preview_video(
271
+ correction_result: CorrectionResult,
272
+ updated_data: Dict[str, Any],
273
+ output_config: OutputConfig,
274
+ audio_filepath: str,
275
+ artist: Optional[str] = None,
276
+ title: Optional[str] = None,
277
+ logger: Optional[logging.Logger] = None
278
+ ) -> Dict[str, Any]:
279
+ """
280
+ Generate a preview video with current corrections.
281
+
282
+ Args:
283
+ correction_result: Current correction result
284
+ updated_data: Updated correction data for preview
285
+ output_config: Output configuration
286
+ audio_filepath: Path to audio file
287
+ artist: Optional artist name
288
+ title: Optional title
289
+ logger: Optional logger instance
290
+
291
+ Returns:
292
+ Dict with status, preview_hash, and video_path
293
+
294
+ Raises:
295
+ ValueError: If preview video generation fails
296
+ """
297
+ if not logger:
298
+ logger = logging.getLogger(__name__)
299
+
300
+ logger.info("Generating preview video with corrected data")
301
+
302
+ # Create temporary correction result with updated data
303
+ temp_correction = CorrectionOperations.update_correction_result_with_data(
304
+ correction_result, updated_data
305
+ )
306
+
307
+ # Generate a unique hash for this preview
308
+ preview_data = json.dumps(updated_data, sort_keys=True).encode("utf-8")
309
+ preview_hash = hashlib.md5(preview_data).hexdigest()[:12]
310
+
311
+ # Set up preview config
312
+ preview_config = OutputConfig(
313
+ output_dir=str(Path(output_config.output_dir) / "previews"),
314
+ cache_dir=output_config.cache_dir,
315
+ output_styles_json=output_config.output_styles_json,
316
+ video_resolution="360p", # Force 360p for preview
317
+ render_video=True,
318
+ generate_cdg=False,
319
+ generate_plain_text=False,
320
+ generate_lrc=False,
321
+ fetch_lyrics=False,
322
+ run_transcription=False,
323
+ run_correction=False,
324
+ )
325
+
326
+ # Create previews directory
327
+ preview_dir = Path(output_config.output_dir) / "previews"
328
+ preview_dir.mkdir(exist_ok=True)
329
+
330
+ # Initialize output generator
331
+ output_generator = OutputGenerator(config=preview_config, logger=logger, preview_mode=True)
332
+
333
+ # Generate preview outputs
334
+ preview_outputs = output_generator.generate_outputs(
335
+ transcription_corrected=temp_correction,
336
+ lyrics_results={}, # Empty dict since we don't need lyrics results for preview
337
+ output_prefix=f"preview_{preview_hash}",
338
+ audio_filepath=audio_filepath,
339
+ artist=artist,
340
+ title=title,
341
+ )
342
+
343
+ if not preview_outputs.video:
344
+ raise ValueError("Preview video generation failed")
345
+
346
+ logger.info(f"Generated preview video: {preview_outputs.video}")
347
+
348
+ return {
349
+ "status": "success",
350
+ "preview_hash": preview_hash,
351
+ "video_path": preview_outputs.video
352
+ }
@@ -2,7 +2,7 @@
2
2
  "name": "lyrics-transcriber-frontend",
3
3
  "private": true,
4
4
  "homepage": "https://nomadkaraoke.github.io/lyrics-transcriber-frontend",
5
- "version": "0.56.1",
5
+ "version": "0.57.0",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "dev": "vite",
@@ -38915,7 +38915,7 @@ const theme = createTheme({
38915
38915
  spacing: (factor) => `${0.6 * factor}rem`
38916
38916
  // Further reduced from 0.8 * factor
38917
38917
  });
38918
- const version = "0.56.1";
38918
+ const version = "0.57.0";
38919
38919
  const packageJson = {
38920
38920
  version
38921
38921
  };
@@ -38926,4 +38926,4 @@ ReactDOM$1.createRoot(document.getElementById("root")).render(
38926
38926
  /* @__PURE__ */ jsxRuntimeExports.jsx(App, {})
38927
38927
  ] })
38928
38928
  );
38929
- //# sourceMappingURL=index-C6nHrD6T.js.map
38929
+ //# sourceMappingURL=index-D-tkyYyP.js.map