lyrics-transcriber 0.41.0__py3-none-any.whl → 0.42.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/core/controller.py +30 -52
- lyrics_transcriber/correction/anchor_sequence.py +325 -150
- lyrics_transcriber/correction/corrector.py +224 -107
- lyrics_transcriber/correction/handlers/base.py +28 -10
- lyrics_transcriber/correction/handlers/extend_anchor.py +47 -24
- lyrics_transcriber/correction/handlers/levenshtein.py +75 -33
- lyrics_transcriber/correction/handlers/llm.py +290 -0
- lyrics_transcriber/correction/handlers/no_space_punct_match.py +81 -36
- lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +46 -26
- lyrics_transcriber/correction/handlers/repeat.py +28 -11
- lyrics_transcriber/correction/handlers/sound_alike.py +68 -32
- lyrics_transcriber/correction/handlers/syllables_match.py +80 -30
- lyrics_transcriber/correction/handlers/word_count_match.py +36 -19
- lyrics_transcriber/correction/handlers/word_operations.py +68 -22
- lyrics_transcriber/correction/text_utils.py +3 -7
- lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
- lyrics_transcriber/frontend/.yarn/releases/yarn-4.6.0.cjs +934 -0
- lyrics_transcriber/frontend/.yarnrc.yml +3 -0
- lyrics_transcriber/frontend/dist/assets/{index-DKnNJHRK.js → index-coH8y7gV.js} +16284 -9032
- lyrics_transcriber/frontend/dist/assets/index-coH8y7gV.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/package.json +6 -2
- lyrics_transcriber/frontend/src/App.tsx +18 -2
- lyrics_transcriber/frontend/src/api.ts +103 -6
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +7 -6
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +86 -59
- lyrics_transcriber/frontend/src/components/EditModal.tsx +93 -43
- lyrics_transcriber/frontend/src/components/FileUpload.tsx +2 -2
- lyrics_transcriber/frontend/src/components/Header.tsx +251 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +303 -265
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +117 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +125 -40
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +129 -115
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +59 -78
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +40 -16
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +4 -10
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +137 -68
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +1 -1
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +85 -115
- lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
- lyrics_transcriber/frontend/src/components/shared/types.ts +15 -7
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +35 -0
- lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +7 -7
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +121 -0
- lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
- lyrics_transcriber/frontend/src/types.js +2 -0
- lyrics_transcriber/frontend/src/types.ts +70 -49
- lyrics_transcriber/frontend/src/validation.ts +132 -0
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/yarn.lock +3752 -0
- lyrics_transcriber/lyrics/base_lyrics_provider.py +75 -12
- lyrics_transcriber/lyrics/file_provider.py +6 -5
- lyrics_transcriber/lyrics/genius.py +5 -2
- lyrics_transcriber/lyrics/spotify.py +58 -21
- lyrics_transcriber/output/ass/config.py +16 -5
- lyrics_transcriber/output/cdg.py +1 -1
- lyrics_transcriber/output/generator.py +22 -8
- lyrics_transcriber/output/plain_text.py +15 -10
- lyrics_transcriber/output/segment_resizer.py +16 -3
- lyrics_transcriber/output/subtitles.py +27 -1
- lyrics_transcriber/output/video.py +107 -1
- lyrics_transcriber/review/__init__.py +0 -1
- lyrics_transcriber/review/server.py +337 -164
- lyrics_transcriber/transcribers/audioshake.py +3 -0
- lyrics_transcriber/transcribers/base_transcriber.py +11 -3
- lyrics_transcriber/transcribers/whisper.py +11 -1
- lyrics_transcriber/types.py +151 -105
- lyrics_transcriber/utils/word_utils.py +27 -0
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/METADATA +3 -1
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/RECORD +74 -61
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/WHEEL +1 -1
- lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js.map +0 -1
- lyrics_transcriber/frontend/package-lock.json +0 -4260
- lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +0 -202
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/entry_points.txt +0 -0
@@ -1,18 +1,30 @@
|
|
1
|
-
from typing import List, Optional, Tuple, Union
|
1
|
+
from typing import List, Optional, Tuple, Union, Dict, Any
|
2
2
|
import logging
|
3
3
|
from pathlib import Path
|
4
|
+
from copy import deepcopy
|
4
5
|
|
6
|
+
from lyrics_transcriber.correction.handlers.levenshtein import LevenshteinHandler
|
7
|
+
from lyrics_transcriber.correction.handlers.llm import LLMHandler
|
5
8
|
from lyrics_transcriber.correction.handlers.no_space_punct_match import NoSpacePunctuationMatchHandler
|
6
9
|
from lyrics_transcriber.correction.handlers.relaxed_word_count_match import RelaxedWordCountMatchHandler
|
10
|
+
from lyrics_transcriber.correction.handlers.repeat import RepeatCorrectionHandler
|
11
|
+
from lyrics_transcriber.correction.handlers.sound_alike import SoundAlikeHandler
|
7
12
|
from lyrics_transcriber.correction.handlers.syllables_match import SyllablesMatchHandler
|
8
|
-
from lyrics_transcriber.
|
13
|
+
from lyrics_transcriber.correction.handlers.word_count_match import WordCountMatchHandler
|
14
|
+
from lyrics_transcriber.types import (
|
15
|
+
CorrectionStep,
|
16
|
+
GapSequence,
|
17
|
+
LyricsData,
|
18
|
+
TranscriptionResult,
|
19
|
+
CorrectionResult,
|
20
|
+
LyricsSegment,
|
21
|
+
WordCorrection,
|
22
|
+
Word,
|
23
|
+
)
|
9
24
|
from lyrics_transcriber.correction.anchor_sequence import AnchorSequenceFinder
|
10
25
|
from lyrics_transcriber.correction.handlers.base import GapCorrectionHandler
|
11
|
-
from lyrics_transcriber.correction.handlers.word_count_match import WordCountMatchHandler
|
12
26
|
from lyrics_transcriber.correction.handlers.extend_anchor import ExtendAnchorHandler
|
13
|
-
from lyrics_transcriber.
|
14
|
-
from lyrics_transcriber.correction.handlers.levenshtein import LevenshteinHandler
|
15
|
-
from lyrics_transcriber.correction.handlers.repeat import RepeatCorrectionHandler
|
27
|
+
from lyrics_transcriber.utils.word_utils import WordUtils
|
16
28
|
|
17
29
|
|
18
30
|
class LyricsCorrector:
|
@@ -24,25 +36,54 @@ class LyricsCorrector:
|
|
24
36
|
self,
|
25
37
|
cache_dir: Union[str, Path],
|
26
38
|
handlers: Optional[List[GapCorrectionHandler]] = None,
|
39
|
+
enabled_handlers: Optional[List[str]] = None,
|
27
40
|
anchor_finder: Optional[AnchorSequenceFinder] = None,
|
28
41
|
logger: Optional[logging.Logger] = None,
|
29
42
|
):
|
30
43
|
self.logger = logger or logging.getLogger(__name__)
|
31
44
|
self._anchor_finder = anchor_finder
|
32
|
-
self._cache_dir = cache_dir
|
33
|
-
|
34
|
-
#
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
# RepeatCorrectionHandler(logger=self.logger),
|
42
|
-
# SoundAlikeHandler(logger=self.logger),
|
43
|
-
# LevenshteinHandler(logger=self.logger),
|
45
|
+
self._cache_dir = Path(cache_dir)
|
46
|
+
|
47
|
+
# Define default enabled handlers - excluding LLM, Repeat, SoundAlike, and Levenshtein
|
48
|
+
DEFAULT_ENABLED_HANDLERS = [
|
49
|
+
"ExtendAnchorHandler",
|
50
|
+
"WordCountMatchHandler",
|
51
|
+
"SyllablesMatchHandler",
|
52
|
+
"RelaxedWordCountMatchHandler",
|
53
|
+
"NoSpacePunctuationMatchHandler",
|
44
54
|
]
|
45
55
|
|
56
|
+
# Create all handlers but respect enabled_handlers if provided
|
57
|
+
all_handlers = [
|
58
|
+
("ExtendAnchorHandler", ExtendAnchorHandler(logger=self.logger)),
|
59
|
+
("WordCountMatchHandler", WordCountMatchHandler(logger=self.logger)),
|
60
|
+
("SyllablesMatchHandler", SyllablesMatchHandler(logger=self.logger)),
|
61
|
+
("RelaxedWordCountMatchHandler", RelaxedWordCountMatchHandler(logger=self.logger)),
|
62
|
+
("NoSpacePunctuationMatchHandler", NoSpacePunctuationMatchHandler(logger=self.logger)),
|
63
|
+
("LLMHandler", LLMHandler(logger=self.logger, cache_dir=self._cache_dir)),
|
64
|
+
("RepeatCorrectionHandler", RepeatCorrectionHandler(logger=self.logger)),
|
65
|
+
("SoundAlikeHandler", SoundAlikeHandler(logger=self.logger)),
|
66
|
+
("LevenshteinHandler", LevenshteinHandler(logger=self.logger)),
|
67
|
+
]
|
68
|
+
|
69
|
+
# Store all handler information
|
70
|
+
self.all_handlers = [
|
71
|
+
{
|
72
|
+
"id": handler_id,
|
73
|
+
"name": handler_id,
|
74
|
+
"description": handler.__class__.__doc__ or "",
|
75
|
+
"enabled": handler_id in (enabled_handlers if enabled_handlers is not None else DEFAULT_ENABLED_HANDLERS),
|
76
|
+
}
|
77
|
+
for handler_id, handler in all_handlers
|
78
|
+
]
|
79
|
+
|
80
|
+
if handlers:
|
81
|
+
self.handlers = handlers
|
82
|
+
else:
|
83
|
+
# Use provided enabled_handlers if available, otherwise use defaults
|
84
|
+
handler_filter = enabled_handlers if enabled_handlers is not None else DEFAULT_ENABLED_HANDLERS
|
85
|
+
self.handlers = [h[1] for h in all_handlers if h[0] in handler_filter]
|
86
|
+
|
46
87
|
@property
|
47
88
|
def anchor_finder(self) -> AnchorSequenceFinder:
|
48
89
|
"""Lazy load the anchor finder instance, initializing it if not already set."""
|
@@ -50,39 +91,52 @@ class LyricsCorrector:
|
|
50
91
|
self._anchor_finder = AnchorSequenceFinder(cache_dir=self._cache_dir, logger=self.logger)
|
51
92
|
return self._anchor_finder
|
52
93
|
|
53
|
-
def run(
|
94
|
+
def run(
|
95
|
+
self,
|
96
|
+
transcription_results: List[TranscriptionResult],
|
97
|
+
lyrics_results: Dict[str, LyricsData],
|
98
|
+
metadata: Optional[Dict[str, Any]] = None,
|
99
|
+
) -> CorrectionResult:
|
54
100
|
"""Execute the correction process."""
|
55
101
|
if not transcription_results:
|
56
102
|
self.logger.error("No transcription results available")
|
57
103
|
raise ValueError("No primary transcription data available")
|
58
104
|
|
105
|
+
# Store reference lyrics for use in word map
|
106
|
+
self.reference_lyrics = lyrics_results
|
107
|
+
|
59
108
|
# Get primary transcription
|
60
109
|
primary_transcription = sorted(transcription_results, key=lambda x: x.priority)[0].result
|
61
110
|
transcribed_text = " ".join(" ".join(w.text for w in segment.words) for segment in primary_transcription.segments)
|
62
|
-
reference_texts = {lyrics.source: lyrics.lyrics for lyrics in lyrics_results}
|
63
111
|
|
64
112
|
# Find anchor sequences and gaps
|
65
113
|
self.logger.debug("Finding anchor sequences and gaps")
|
66
|
-
anchor_sequences = self.anchor_finder.find_anchors(transcribed_text,
|
67
|
-
gap_sequences = self.anchor_finder.find_gaps(transcribed_text, anchor_sequences,
|
114
|
+
anchor_sequences = self.anchor_finder.find_anchors(transcribed_text, lyrics_results, primary_transcription)
|
115
|
+
gap_sequences = self.anchor_finder.find_gaps(transcribed_text, anchor_sequences, lyrics_results, primary_transcription)
|
116
|
+
|
117
|
+
# Store anchor sequences for use in correction handlers
|
118
|
+
self._anchor_sequences = anchor_sequences
|
68
119
|
|
69
|
-
# Process corrections
|
70
|
-
corrections, corrected_segments = self._process_corrections(
|
120
|
+
# Process corrections with metadata
|
121
|
+
corrections, corrected_segments, correction_steps, word_id_map, segment_id_map = self._process_corrections(
|
122
|
+
primary_transcription.segments, gap_sequences, metadata=metadata
|
123
|
+
)
|
71
124
|
|
72
125
|
# Calculate correction ratio
|
73
126
|
total_words = sum(len(segment.words) for segment in corrected_segments)
|
74
127
|
corrections_made = len(corrections)
|
75
128
|
correction_ratio = 1 - (corrections_made / total_words if total_words > 0 else 0)
|
76
129
|
|
130
|
+
# Get the currently enabled handler IDs using full class names
|
131
|
+
enabled_handlers = [handler.__class__.__name__ for handler in self.handlers]
|
132
|
+
|
77
133
|
return CorrectionResult(
|
78
134
|
original_segments=primary_transcription.segments,
|
79
135
|
corrected_segments=corrected_segments,
|
80
|
-
corrected_text="\n".join(segment.text for segment in corrected_segments) + "\n",
|
81
136
|
corrections=corrections,
|
82
137
|
corrections_made=corrections_made,
|
83
138
|
confidence=correction_ratio,
|
84
|
-
|
85
|
-
reference_texts=reference_texts,
|
139
|
+
reference_lyrics=lyrics_results,
|
86
140
|
anchor_sequences=anchor_sequences,
|
87
141
|
resized_segments=[],
|
88
142
|
gap_sequences=gap_sequences,
|
@@ -91,7 +145,12 @@ class LyricsCorrector:
|
|
91
145
|
"gap_sequences_count": len(gap_sequences),
|
92
146
|
"total_words": total_words,
|
93
147
|
"correction_ratio": correction_ratio,
|
148
|
+
"available_handlers": self.all_handlers,
|
149
|
+
"enabled_handlers": enabled_handlers,
|
94
150
|
},
|
151
|
+
correction_steps=correction_steps,
|
152
|
+
word_id_map=word_id_map,
|
153
|
+
segment_id_map=segment_id_map,
|
95
154
|
)
|
96
155
|
|
97
156
|
def _preserve_formatting(self, original: str, new_word: str) -> str:
|
@@ -102,8 +161,8 @@ class LyricsCorrector:
|
|
102
161
|
return leading_space + new_word.strip() + trailing_space
|
103
162
|
|
104
163
|
def _process_corrections(
|
105
|
-
self, segments: List[LyricsSegment], gap_sequences: List[GapSequence]
|
106
|
-
) -> Tuple[List[WordCorrection], List[LyricsSegment]]:
|
164
|
+
self, segments: List[LyricsSegment], gap_sequences: List[GapSequence], metadata: Optional[Dict[str, Any]] = None
|
165
|
+
) -> Tuple[List[WordCorrection], List[LyricsSegment], List[CorrectionStep], Dict[str, str], Dict[str, str]]:
|
107
166
|
"""Process corrections using handlers.
|
108
167
|
|
109
168
|
The correction flow works as follows:
|
@@ -121,84 +180,109 @@ class LyricsCorrector:
|
|
121
180
|
b) Applying those corrections to the original text (segment-centric)
|
122
181
|
"""
|
123
182
|
self.logger.info(f"Starting correction process with {len(gap_sequences)} gaps")
|
124
|
-
|
125
|
-
# First pass: Process all gaps
|
126
|
-
all_corrections = self._process_gaps(gap_sequences)
|
127
|
-
|
128
|
-
# Second pass: Apply corrections to segments
|
129
|
-
corrected_segments = self._apply_corrections_to_segments(segments, all_corrections)
|
130
|
-
|
131
|
-
self.logger.info(f"Correction process complete. Made {len(all_corrections)} corrections")
|
132
|
-
return all_corrections, corrected_segments
|
133
|
-
|
134
|
-
def _process_gaps(self, gap_sequences: List[GapSequence]) -> List[WordCorrection]:
|
135
|
-
"""Process each gap using available handlers until all words are corrected or no handlers remain."""
|
183
|
+
correction_steps = []
|
136
184
|
all_corrections = []
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
185
|
+
word_id_map = {}
|
186
|
+
segment_id_map = {}
|
187
|
+
|
188
|
+
# Create word map for handlers - include both transcribed and reference words
|
189
|
+
word_map = {w.id: w for s in segments for w in s.words} # Transcribed words
|
190
|
+
|
191
|
+
# Add reference words from all sources
|
192
|
+
for source, lyrics_data in self.reference_lyrics.items():
|
193
|
+
for segment in lyrics_data.segments:
|
194
|
+
for word in segment.words:
|
195
|
+
if word.id not in word_map: # Don't overwrite transcribed words
|
196
|
+
word_map[word.id] = word
|
197
|
+
|
198
|
+
# Base handler data that all handlers need
|
199
|
+
base_handler_data = {
|
200
|
+
"word_map": word_map,
|
201
|
+
"anchor_sequences": self._anchor_sequences,
|
202
|
+
"audio_file_hash": metadata.get("audio_file_hash") if metadata else None,
|
203
|
+
}
|
204
|
+
|
205
|
+
for i, gap in enumerate(gap_sequences, 1):
|
206
|
+
self.logger.info(f"Processing gap {i}/{len(gap_sequences)} at position {gap.transcription_position}")
|
207
|
+
|
208
|
+
# Get the actual words for logging
|
209
|
+
gap_words = [word_map[word_id] for word_id in gap.transcribed_word_ids]
|
210
|
+
self.logger.debug(f"Gap text: '{' '.join(w.text for w in gap_words)}'")
|
211
|
+
|
212
|
+
# Try each handler in order
|
145
213
|
for handler in self.handlers:
|
146
|
-
|
147
|
-
|
148
|
-
uncorrected_positions -= corrected_positions # Skip any corrected positions
|
214
|
+
handler_name = handler.__class__.__name__
|
215
|
+
can_handle, handler_data = handler.can_handle(gap, base_handler_data)
|
149
216
|
|
150
|
-
if
|
151
|
-
|
152
|
-
|
217
|
+
if can_handle:
|
218
|
+
# Merge base handler data with specific handler data
|
219
|
+
handler_data = {**base_handler_data, **(handler_data or {})}
|
153
220
|
|
154
|
-
|
221
|
+
corrections = handler.handle(gap, handler_data)
|
222
|
+
if corrections:
|
223
|
+
self.logger.info(f"Handler {handler_name} made {len(corrections)} corrections")
|
224
|
+
# Track affected IDs
|
225
|
+
affected_word_ids = [w.id for w in self._get_affected_words(gap, segments)]
|
226
|
+
affected_segment_ids = [s.id for s in self._get_affected_segments(gap, segments)]
|
155
227
|
|
156
|
-
|
157
|
-
|
158
|
-
handler.set_previous_corrections(all_corrections)
|
228
|
+
# Apply corrections and get updated segments
|
229
|
+
updated_segments = self._apply_corrections_to_segments(self._get_affected_segments(gap, segments), corrections)
|
159
230
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
231
|
+
# Update ID maps
|
232
|
+
for correction in corrections:
|
233
|
+
if correction.word_id and correction.corrected_word_id:
|
234
|
+
word_id_map[correction.word_id] = correction.corrected_word_id
|
235
|
+
|
236
|
+
# Map segment IDs
|
237
|
+
for old_seg, new_seg in zip(self._get_affected_segments(gap, segments), updated_segments):
|
238
|
+
segment_id_map[old_seg.id] = new_seg.id
|
239
|
+
|
240
|
+
# Create correction step
|
241
|
+
step = CorrectionStep(
|
242
|
+
handler_name=handler_name,
|
243
|
+
affected_word_ids=affected_word_ids,
|
244
|
+
affected_segment_ids=affected_segment_ids,
|
245
|
+
corrections=corrections,
|
246
|
+
segments_before=self._get_affected_segments(gap, segments),
|
247
|
+
segments_after=updated_segments,
|
248
|
+
created_word_ids=[w.id for w in self._get_new_words(updated_segments, affected_word_ids)],
|
249
|
+
deleted_word_ids=[id for id in affected_word_ids if not self._word_exists(id, updated_segments)],
|
250
|
+
)
|
251
|
+
correction_steps.append(step)
|
252
|
+
all_corrections.extend(corrections)
|
253
|
+
|
254
|
+
# Log correction details
|
167
255
|
for correction in corrections:
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
gap.add_correction(correction)
|
173
|
-
corrected_positions.add(correction.original_position)
|
174
|
-
# Track positions with high confidence corrections (>= 0.9)
|
175
|
-
if correction.confidence >= 0.9:
|
176
|
-
high_confidence_positions.add(correction.original_position)
|
177
|
-
|
178
|
-
# Filter out corrections for already corrected positions
|
179
|
-
new_corrections = [c for c in corrections if c.original_position in corrected_positions]
|
180
|
-
if new_corrections:
|
181
|
-
self.logger.debug(
|
182
|
-
f"{handler.__class__.__name__} made {len(new_corrections)} corrections: "
|
183
|
-
f"{[f'{c.original_word}->{c.corrected_word}' for c in new_corrections]}"
|
256
|
+
self.logger.info(
|
257
|
+
f"Made correction: '{correction.original_word}' -> '{correction.corrected_word}' "
|
258
|
+
f"(confidence: {correction.confidence:.2f}, reason: {correction.reason})"
|
184
259
|
)
|
185
|
-
|
260
|
+
break # Stop trying other handlers once we've made corrections
|
261
|
+
else:
|
262
|
+
self.logger.debug(f"Handler {handler_name} found no corrections needed")
|
263
|
+
else:
|
264
|
+
self.logger.debug(f"Handler {handler_name} cannot handle gap")
|
186
265
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
self.logger.debug(f"Uncorrected words remaining: {', '.join(uncorrected)}")
|
266
|
+
# Create final result with correction history
|
267
|
+
corrected_segments = self._apply_all_corrections(segments, all_corrections)
|
268
|
+
self.logger.info(f"Correction process completed with {len(all_corrections)} total corrections")
|
269
|
+
return all_corrections, corrected_segments, correction_steps, word_id_map, segment_id_map
|
192
270
|
|
193
|
-
|
194
|
-
|
271
|
+
def _get_new_words(self, segments: List[LyricsSegment], original_word_ids: List[str]) -> List[Word]:
|
272
|
+
"""Find words that were created during correction."""
|
273
|
+
return [w for s in segments for w in s.words if w.id not in original_word_ids]
|
195
274
|
|
196
|
-
|
275
|
+
def _word_exists(self, word_id: str, segments: List[LyricsSegment]) -> bool:
|
276
|
+
"""Check if a word ID still exists in the segments."""
|
277
|
+
return any(w.id == word_id for s in segments for w in s.words)
|
197
278
|
|
198
279
|
def _apply_corrections_to_segments(self, segments: List[LyricsSegment], corrections: List[WordCorrection]) -> List[LyricsSegment]:
|
199
280
|
"""Apply corrections to create new segments."""
|
200
|
-
|
281
|
+
# Create word ID map for quick lookup
|
282
|
+
word_map = {w.id: w for s in segments for w in s.words}
|
283
|
+
|
201
284
|
# Group corrections by original_position to handle splits
|
285
|
+
correction_map = {}
|
202
286
|
for c in corrections:
|
203
287
|
if c.original_position not in correction_map:
|
204
288
|
correction_map[c.original_position] = []
|
@@ -207,7 +291,7 @@ class LyricsCorrector:
|
|
207
291
|
corrected_segments = []
|
208
292
|
current_word_idx = 0
|
209
293
|
|
210
|
-
for
|
294
|
+
for segment in segments:
|
211
295
|
corrected_words = []
|
212
296
|
for word in segment.words:
|
213
297
|
if current_word_idx in correction_map:
|
@@ -226,28 +310,30 @@ class LyricsCorrector:
|
|
226
310
|
|
227
311
|
# Update corrected_position as we create new words
|
228
312
|
correction.corrected_position = len(corrected_words)
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
313
|
+
new_word = Word(
|
314
|
+
id=correction.corrected_word_id or WordUtils.generate_id(),
|
315
|
+
text=self._preserve_formatting(correction.original_word, correction.corrected_word),
|
316
|
+
start_time=start_time,
|
317
|
+
end_time=end_time,
|
318
|
+
confidence=correction.confidence,
|
319
|
+
created_during_correction=True,
|
236
320
|
)
|
321
|
+
corrected_words.append(new_word)
|
237
322
|
else:
|
238
323
|
# Handle single word replacement
|
239
324
|
correction = word_corrections[0]
|
240
325
|
if not correction.is_deletion:
|
241
326
|
# Update corrected_position
|
242
327
|
correction.corrected_position = len(corrected_words)
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
328
|
+
new_word = Word(
|
329
|
+
id=correction.corrected_word_id or WordUtils.generate_id(),
|
330
|
+
text=self._preserve_formatting(correction.original_word, correction.corrected_word),
|
331
|
+
start_time=word.start_time,
|
332
|
+
end_time=word.end_time,
|
333
|
+
confidence=correction.confidence,
|
334
|
+
created_during_correction=True,
|
250
335
|
)
|
336
|
+
corrected_words.append(new_word)
|
251
337
|
else:
|
252
338
|
corrected_words.append(word)
|
253
339
|
current_word_idx += 1
|
@@ -255,6 +341,7 @@ class LyricsCorrector:
|
|
255
341
|
if corrected_words:
|
256
342
|
corrected_segments.append(
|
257
343
|
LyricsSegment(
|
344
|
+
id=segment.id, # Preserve original segment ID
|
258
345
|
text=" ".join(w.text for w in corrected_words),
|
259
346
|
words=corrected_words,
|
260
347
|
start_time=segment.start_time,
|
@@ -263,3 +350,33 @@ class LyricsCorrector:
|
|
263
350
|
)
|
264
351
|
|
265
352
|
return corrected_segments
|
353
|
+
|
354
|
+
def _get_affected_segments(self, gap: GapSequence, segments: List[LyricsSegment]) -> List[LyricsSegment]:
|
355
|
+
"""Get segments that contain words from the gap sequence."""
|
356
|
+
affected_segments = []
|
357
|
+
gap_word_ids = set(gap.transcribed_word_ids)
|
358
|
+
|
359
|
+
for segment in segments:
|
360
|
+
# Check if any words in this segment are part of the gap
|
361
|
+
if any(w.id in gap_word_ids for w in segment.words):
|
362
|
+
affected_segments.append(segment)
|
363
|
+
elif affected_segments: # We've passed the gap
|
364
|
+
break
|
365
|
+
|
366
|
+
return affected_segments
|
367
|
+
|
368
|
+
def _get_affected_words(self, gap: GapSequence, segments: List[LyricsSegment]) -> List[Word]:
|
369
|
+
"""Get words that are part of the gap sequence."""
|
370
|
+
# Create a map of word IDs to Word objects for quick lookup
|
371
|
+
word_map = {w.id: w for s in segments for w in s.words}
|
372
|
+
|
373
|
+
# Get the actual Word objects using the IDs
|
374
|
+
return [word_map[word_id] for word_id in gap.transcribed_word_ids]
|
375
|
+
|
376
|
+
def _apply_all_corrections(self, segments: List[LyricsSegment], corrections: List[WordCorrection]) -> List[LyricsSegment]:
|
377
|
+
"""Apply all corrections to create final corrected segments."""
|
378
|
+
# Make a deep copy to avoid modifying original segments
|
379
|
+
working_segments = deepcopy(segments)
|
380
|
+
|
381
|
+
# Apply corrections in order
|
382
|
+
return self._apply_corrections_to_segments(working_segments, corrections)
|
@@ -12,23 +12,41 @@ class GapCorrectionHandler(ABC):
|
|
12
12
|
self.logger = logger or logging.getLogger(__name__)
|
13
13
|
|
14
14
|
@abstractmethod
|
15
|
-
def can_handle(self, gap: GapSequence) -> Tuple[bool, Dict[str, Any]]:
|
16
|
-
"""
|
17
|
-
|
15
|
+
def can_handle(self, gap: GapSequence, data: Optional[Dict[str, Any]] = None) -> Tuple[bool, Dict[str, Any]]:
|
16
|
+
"""Check if this handler can process the given gap.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
gap: The gap sequence to check
|
20
|
+
data: Optional dictionary containing additional data like word_map
|
21
|
+
|
18
22
|
Returns:
|
19
|
-
Tuple
|
20
|
-
- bool: Whether this handler can process the gap
|
21
|
-
- dict: Data computed during can_handle that will be needed by handle().
|
22
|
-
Empty dict if no data needs to be passed.
|
23
|
+
Tuple of (can_handle, handler_data)
|
23
24
|
"""
|
24
25
|
pass
|
25
26
|
|
26
27
|
@abstractmethod
|
27
28
|
def handle(self, gap: GapSequence, data: Optional[Dict[str, Any]] = None) -> List[WordCorrection]:
|
28
|
-
"""Process
|
29
|
-
|
29
|
+
"""Process the gap and return any corrections.
|
30
|
+
|
30
31
|
Args:
|
31
32
|
gap: The gap sequence to process
|
32
|
-
data: Optional
|
33
|
+
data: Optional dictionary containing additional data like word_map
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
List of corrections to apply
|
33
37
|
"""
|
34
38
|
pass
|
39
|
+
|
40
|
+
def _validate_data(self, data: Optional[Dict[str, Any]]) -> bool:
|
41
|
+
"""Validate that required data is present.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
data: The data dictionary to validate
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
True if data is valid, False otherwise
|
48
|
+
"""
|
49
|
+
if not data or "word_map" not in data:
|
50
|
+
self.logger.error("No word_map provided in data")
|
51
|
+
return False
|
52
|
+
return True
|
@@ -1,7 +1,7 @@
|
|
1
1
|
from typing import List, Optional, Tuple, Dict, Any
|
2
2
|
import logging
|
3
3
|
|
4
|
-
from lyrics_transcriber.types import GapSequence, WordCorrection
|
4
|
+
from lyrics_transcriber.types import GapSequence, WordCorrection, Word
|
5
5
|
from lyrics_transcriber.correction.handlers.base import GapCorrectionHandler
|
6
6
|
from lyrics_transcriber.correction.handlers.word_operations import WordOperations
|
7
7
|
|
@@ -40,26 +40,31 @@ class ExtendAnchorHandler(GapCorrectionHandler):
|
|
40
40
|
"""
|
41
41
|
|
42
42
|
def __init__(self, logger: Optional[logging.Logger] = None):
|
43
|
-
super().__init__(logger)
|
44
43
|
self.logger = logger or logging.getLogger(__name__)
|
45
44
|
|
46
|
-
def can_handle(self, gap: GapSequence) -> Tuple[bool, Dict[str, Any]]:
|
47
|
-
|
48
|
-
if
|
49
|
-
|
45
|
+
def can_handle(self, gap: GapSequence, data: Optional[Dict[str, Any]] = None) -> Tuple[bool, Dict[str, Any]]:
|
46
|
+
"""Check if this gap can be handled by extending anchor sequences."""
|
47
|
+
# Check if we have anchor sequences
|
48
|
+
if not data or "anchor_sequences" not in data:
|
49
|
+
self.logger.debug("No anchor sequences available")
|
50
50
|
return False, {}
|
51
51
|
|
52
|
-
#
|
53
|
-
if not gap.
|
54
|
-
self.logger.debug("No
|
52
|
+
# Must have reference word IDs
|
53
|
+
if not gap.reference_word_ids:
|
54
|
+
self.logger.debug("No reference word IDs available.")
|
55
55
|
return False, {}
|
56
56
|
|
57
|
-
#
|
57
|
+
# Gap must have word IDs
|
58
|
+
if not gap.transcribed_word_ids:
|
59
|
+
self.logger.debug("No word IDs in the gap to process.")
|
60
|
+
return False, {}
|
61
|
+
|
62
|
+
# At least one word ID must match between gap and any reference source
|
58
63
|
# in the same position
|
59
64
|
has_match = any(
|
60
|
-
i < len(
|
61
|
-
for
|
62
|
-
for i in range(min(len(gap.
|
65
|
+
i < len(ref_word_ids) and gap.transcribed_word_ids[i] == ref_word_ids[i]
|
66
|
+
for ref_word_ids in gap.reference_word_ids.values()
|
67
|
+
for i in range(min(len(gap.transcribed_word_ids), len(ref_word_ids)))
|
63
68
|
)
|
64
69
|
|
65
70
|
self.logger.debug(f"Can handle gap: {has_match}")
|
@@ -68,16 +73,32 @@ class ExtendAnchorHandler(GapCorrectionHandler):
|
|
68
73
|
def handle(self, gap: GapSequence, data: Optional[Dict[str, Any]] = None) -> List[WordCorrection]:
|
69
74
|
corrections = []
|
70
75
|
|
76
|
+
# Get word lookup map from data
|
77
|
+
word_map = data.get("word_map", {})
|
78
|
+
if not word_map:
|
79
|
+
self.logger.error("No word_map provided in data")
|
80
|
+
return []
|
81
|
+
|
71
82
|
# Process each word in the gap that has a corresponding reference position
|
72
|
-
for i,
|
83
|
+
for i, word_id in enumerate(gap.transcribed_word_ids):
|
84
|
+
# Get the actual word object
|
85
|
+
if word_id not in word_map:
|
86
|
+
self.logger.error(f"Word ID {word_id} not found in word_map")
|
87
|
+
continue
|
88
|
+
word = word_map[word_id]
|
89
|
+
|
73
90
|
# Find reference sources that have a matching word at this position
|
74
91
|
matching_sources = [
|
75
|
-
source for source,
|
92
|
+
source for source, ref_word_ids in gap.reference_word_ids.items() if i < len(ref_word_ids) and word_id == ref_word_ids[i]
|
76
93
|
]
|
77
94
|
|
95
|
+
if not matching_sources:
|
96
|
+
self.logger.debug(f"Skipping word '{word.text}' at position {i} - no matching references")
|
97
|
+
continue
|
98
|
+
|
78
99
|
if matching_sources:
|
79
100
|
# Word matches reference(s) at this position - validate it
|
80
|
-
confidence = len(matching_sources) / len(gap.
|
101
|
+
confidence = len(matching_sources) / len(gap.reference_word_ids)
|
81
102
|
sources = ", ".join(matching_sources)
|
82
103
|
|
83
104
|
# Get base reference positions
|
@@ -88,24 +109,26 @@ class ExtendAnchorHandler(GapCorrectionHandler):
|
|
88
109
|
for source in matching_sources:
|
89
110
|
if source in base_reference_positions:
|
90
111
|
# Find this word's position in the reference text
|
91
|
-
|
92
|
-
for ref_idx,
|
93
|
-
if
|
112
|
+
ref_word_ids = gap.reference_word_ids[source]
|
113
|
+
for ref_idx, ref_word_id in enumerate(ref_word_ids):
|
114
|
+
if ref_word_id == word_id:
|
94
115
|
reference_positions[source] = base_reference_positions[source] + ref_idx
|
95
116
|
break
|
96
117
|
|
97
118
|
corrections.append(
|
98
119
|
WordOperations.create_word_replacement_correction(
|
99
|
-
original_word=word,
|
100
|
-
corrected_word=word,
|
120
|
+
original_word=word.text,
|
121
|
+
corrected_word=word.text,
|
101
122
|
original_position=gap.transcription_position + i,
|
102
123
|
source=sources,
|
103
124
|
confidence=confidence,
|
104
|
-
reason="
|
125
|
+
reason="Matched reference source(s)",
|
105
126
|
reference_positions=reference_positions,
|
127
|
+
handler="ExtendAnchorHandler",
|
128
|
+
original_word_id=word_id,
|
129
|
+
corrected_word_id=word_id,
|
106
130
|
)
|
107
131
|
)
|
108
|
-
self.logger.debug(f"Validated word '{word}' with confidence {confidence} from sources: {sources}")
|
109
|
-
# No else clause - non-matching words are left unchanged
|
132
|
+
self.logger.debug(f"Validated word '{word.text}' with confidence {confidence} from sources: {sources}")
|
110
133
|
|
111
134
|
return corrections
|