karaoke-gen 0.75.54__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.
Potentially problematic release.
This version of karaoke-gen might be problematic. Click here for more details.
- karaoke_gen/__init__.py +38 -0
- karaoke_gen/audio_fetcher.py +1614 -0
- karaoke_gen/audio_processor.py +790 -0
- karaoke_gen/config.py +83 -0
- karaoke_gen/file_handler.py +387 -0
- karaoke_gen/instrumental_review/__init__.py +45 -0
- karaoke_gen/instrumental_review/analyzer.py +408 -0
- karaoke_gen/instrumental_review/editor.py +322 -0
- karaoke_gen/instrumental_review/models.py +171 -0
- karaoke_gen/instrumental_review/server.py +475 -0
- karaoke_gen/instrumental_review/static/index.html +1529 -0
- karaoke_gen/instrumental_review/waveform.py +409 -0
- karaoke_gen/karaoke_finalise/__init__.py +1 -0
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +1833 -0
- karaoke_gen/karaoke_gen.py +1026 -0
- karaoke_gen/lyrics_processor.py +474 -0
- karaoke_gen/metadata.py +160 -0
- karaoke_gen/pipeline/__init__.py +87 -0
- karaoke_gen/pipeline/base.py +215 -0
- karaoke_gen/pipeline/context.py +230 -0
- karaoke_gen/pipeline/executors/__init__.py +21 -0
- karaoke_gen/pipeline/executors/local.py +159 -0
- karaoke_gen/pipeline/executors/remote.py +257 -0
- karaoke_gen/pipeline/stages/__init__.py +27 -0
- karaoke_gen/pipeline/stages/finalize.py +202 -0
- karaoke_gen/pipeline/stages/render.py +165 -0
- karaoke_gen/pipeline/stages/screens.py +139 -0
- karaoke_gen/pipeline/stages/separation.py +191 -0
- karaoke_gen/pipeline/stages/transcription.py +191 -0
- karaoke_gen/resources/AvenirNext-Bold.ttf +0 -0
- karaoke_gen/resources/Montserrat-Bold.ttf +0 -0
- karaoke_gen/resources/Oswald-Bold.ttf +0 -0
- karaoke_gen/resources/Oswald-SemiBold.ttf +0 -0
- karaoke_gen/resources/Zurich_Cn_BT_Bold.ttf +0 -0
- karaoke_gen/style_loader.py +531 -0
- karaoke_gen/utils/__init__.py +18 -0
- karaoke_gen/utils/bulk_cli.py +492 -0
- karaoke_gen/utils/cli_args.py +432 -0
- karaoke_gen/utils/gen_cli.py +978 -0
- karaoke_gen/utils/remote_cli.py +3268 -0
- karaoke_gen/video_background_processor.py +351 -0
- karaoke_gen/video_generator.py +424 -0
- karaoke_gen-0.75.54.dist-info/METADATA +718 -0
- karaoke_gen-0.75.54.dist-info/RECORD +287 -0
- karaoke_gen-0.75.54.dist-info/WHEEL +4 -0
- karaoke_gen-0.75.54.dist-info/entry_points.txt +5 -0
- karaoke_gen-0.75.54.dist-info/licenses/LICENSE +21 -0
- lyrics_transcriber/__init__.py +10 -0
- lyrics_transcriber/cli/__init__.py +0 -0
- lyrics_transcriber/cli/cli_main.py +285 -0
- lyrics_transcriber/core/__init__.py +0 -0
- lyrics_transcriber/core/config.py +50 -0
- lyrics_transcriber/core/controller.py +594 -0
- lyrics_transcriber/correction/__init__.py +0 -0
- lyrics_transcriber/correction/agentic/__init__.py +9 -0
- lyrics_transcriber/correction/agentic/adapter.py +71 -0
- lyrics_transcriber/correction/agentic/agent.py +313 -0
- lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
- lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
- lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
- lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
- lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
- lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
- lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
- lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
- lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
- lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
- lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
- lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
- lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
- lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
- lyrics_transcriber/correction/agentic/models/enums.py +38 -0
- lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
- lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
- lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
- lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
- lyrics_transcriber/correction/agentic/models/utils.py +19 -0
- lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
- lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
- lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
- lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
- lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
- lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
- lyrics_transcriber/correction/agentic/providers/base.py +36 -0
- lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
- lyrics_transcriber/correction/agentic/providers/config.py +73 -0
- lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
- lyrics_transcriber/correction/agentic/providers/health.py +28 -0
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
- lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
- lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
- lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
- lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
- lyrics_transcriber/correction/agentic/router.py +35 -0
- lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
- lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
- lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
- lyrics_transcriber/correction/anchor_sequence.py +919 -0
- lyrics_transcriber/correction/corrector.py +760 -0
- lyrics_transcriber/correction/feedback/__init__.py +2 -0
- lyrics_transcriber/correction/feedback/schemas.py +107 -0
- lyrics_transcriber/correction/feedback/store.py +236 -0
- lyrics_transcriber/correction/handlers/__init__.py +0 -0
- lyrics_transcriber/correction/handlers/base.py +52 -0
- lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
- lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
- lyrics_transcriber/correction/handlers/llm.py +293 -0
- lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
- lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
- lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
- lyrics_transcriber/correction/handlers/repeat.py +88 -0
- lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
- lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
- lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
- lyrics_transcriber/correction/handlers/word_operations.py +187 -0
- lyrics_transcriber/correction/operations.py +352 -0
- lyrics_transcriber/correction/phrase_analyzer.py +435 -0
- lyrics_transcriber/correction/text_utils.py +30 -0
- lyrics_transcriber/frontend/.gitignore +23 -0
- lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
- lyrics_transcriber/frontend/.yarnrc.yml +3 -0
- lyrics_transcriber/frontend/README.md +50 -0
- lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
- lyrics_transcriber/frontend/__init__.py +25 -0
- lyrics_transcriber/frontend/eslint.config.js +28 -0
- lyrics_transcriber/frontend/index.html +18 -0
- lyrics_transcriber/frontend/package.json +42 -0
- lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
- lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
- lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
- lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
- lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
- lyrics_transcriber/frontend/public/favicon.ico +0 -0
- lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
- lyrics_transcriber/frontend/src/App.tsx +214 -0
- lyrics_transcriber/frontend/src/api.ts +254 -0
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
- lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
- lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
- lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
- lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
- lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
- lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
- lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
- lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
- lyrics_transcriber/frontend/src/components/Header.tsx +413 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1387 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +185 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +704 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +80 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +905 -0
- lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
- lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +127 -0
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
- lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
- lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +336 -0
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
- lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
- lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
- lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
- lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
- lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
- lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
- lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
- lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
- lyrics_transcriber/frontend/src/main.tsx +17 -0
- lyrics_transcriber/frontend/src/theme.ts +177 -0
- lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
- lyrics_transcriber/frontend/src/types.js +2 -0
- lyrics_transcriber/frontend/src/types.ts +199 -0
- lyrics_transcriber/frontend/src/validation.ts +132 -0
- lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
- lyrics_transcriber/frontend/tsconfig.app.json +26 -0
- lyrics_transcriber/frontend/tsconfig.json +25 -0
- lyrics_transcriber/frontend/tsconfig.node.json +23 -0
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
- lyrics_transcriber/frontend/update_version.js +11 -0
- lyrics_transcriber/frontend/vite.config.d.ts +2 -0
- lyrics_transcriber/frontend/vite.config.js +10 -0
- lyrics_transcriber/frontend/vite.config.ts +11 -0
- lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
- lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
- lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
- lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js +43288 -0
- lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +1 -0
- lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
- lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
- lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
- lyrics_transcriber/frontend/web_assets/index.html +18 -0
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
- lyrics_transcriber/frontend/yarn.lock +3752 -0
- lyrics_transcriber/lyrics/__init__.py +0 -0
- lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
- lyrics_transcriber/lyrics/file_provider.py +95 -0
- lyrics_transcriber/lyrics/genius.py +384 -0
- lyrics_transcriber/lyrics/lrclib.py +231 -0
- lyrics_transcriber/lyrics/musixmatch.py +156 -0
- lyrics_transcriber/lyrics/spotify.py +290 -0
- lyrics_transcriber/lyrics/user_input_provider.py +44 -0
- lyrics_transcriber/output/__init__.py +0 -0
- lyrics_transcriber/output/ass/__init__.py +21 -0
- lyrics_transcriber/output/ass/ass.py +2088 -0
- lyrics_transcriber/output/ass/ass_specs.txt +732 -0
- lyrics_transcriber/output/ass/config.py +180 -0
- lyrics_transcriber/output/ass/constants.py +23 -0
- lyrics_transcriber/output/ass/event.py +94 -0
- lyrics_transcriber/output/ass/formatters.py +132 -0
- lyrics_transcriber/output/ass/lyrics_line.py +265 -0
- lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
- lyrics_transcriber/output/ass/section_detector.py +89 -0
- lyrics_transcriber/output/ass/section_screen.py +106 -0
- lyrics_transcriber/output/ass/style.py +187 -0
- lyrics_transcriber/output/cdg.py +619 -0
- lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
- lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
- lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
- lyrics_transcriber/output/cdgmaker/config.py +151 -0
- lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
- lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
- lyrics_transcriber/output/cdgmaker/pack.py +507 -0
- lyrics_transcriber/output/cdgmaker/render.py +346 -0
- lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
- lyrics_transcriber/output/cdgmaker/utils.py +132 -0
- lyrics_transcriber/output/countdown_processor.py +306 -0
- lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
- lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
- lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/arial.ttf +0 -0
- lyrics_transcriber/output/fonts/georgia.ttf +0 -0
- lyrics_transcriber/output/fonts/verdana.ttf +0 -0
- lyrics_transcriber/output/generator.py +257 -0
- lyrics_transcriber/output/lrc_to_cdg.py +61 -0
- lyrics_transcriber/output/lyrics_file.py +102 -0
- lyrics_transcriber/output/plain_text.py +96 -0
- lyrics_transcriber/output/segment_resizer.py +431 -0
- lyrics_transcriber/output/subtitles.py +397 -0
- lyrics_transcriber/output/video.py +544 -0
- lyrics_transcriber/review/__init__.py +0 -0
- lyrics_transcriber/review/server.py +676 -0
- lyrics_transcriber/storage/__init__.py +0 -0
- lyrics_transcriber/storage/dropbox.py +225 -0
- lyrics_transcriber/transcribers/__init__.py +0 -0
- lyrics_transcriber/transcribers/audioshake.py +379 -0
- lyrics_transcriber/transcribers/base_transcriber.py +157 -0
- lyrics_transcriber/transcribers/whisper.py +330 -0
- lyrics_transcriber/types.py +650 -0
- lyrics_transcriber/utils/__init__.py +0 -0
- lyrics_transcriber/utils/word_utils.py +27 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
from dataclasses import dataclass, asdict, field, fields
|
|
2
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from lyrics_transcriber.utils.word_utils import WordUtils
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Word:
|
|
9
|
+
"""Represents a single word with its timing (in seconds) and confidence information."""
|
|
10
|
+
|
|
11
|
+
id: str # New: Unique identifier for each word
|
|
12
|
+
text: str
|
|
13
|
+
start_time: float
|
|
14
|
+
end_time: float
|
|
15
|
+
confidence: Optional[float] = None
|
|
16
|
+
# New: Track if this word was created during correction
|
|
17
|
+
created_during_correction: bool = False
|
|
18
|
+
|
|
19
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
20
|
+
"""Convert Word to dictionary for JSON serialization."""
|
|
21
|
+
d = asdict(self)
|
|
22
|
+
# Remove confidence from output if it's None
|
|
23
|
+
if d["confidence"] is None:
|
|
24
|
+
del d["confidence"]
|
|
25
|
+
return d
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Word":
|
|
29
|
+
"""Create Word from dictionary."""
|
|
30
|
+
return cls(
|
|
31
|
+
id=data["id"],
|
|
32
|
+
text=data["text"],
|
|
33
|
+
start_time=data["start_time"],
|
|
34
|
+
end_time=data["end_time"],
|
|
35
|
+
confidence=data.get("confidence"), # Use get() since confidence is optional
|
|
36
|
+
created_during_correction=data.get("created_during_correction", False),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class LyricsSegment:
|
|
42
|
+
"""Represents a segment/line of lyrics with timing information in seconds."""
|
|
43
|
+
|
|
44
|
+
id: str # New: Unique identifier for each segment
|
|
45
|
+
text: str
|
|
46
|
+
words: List[Word]
|
|
47
|
+
start_time: float
|
|
48
|
+
end_time: float
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
51
|
+
"""Convert LyricsSegment to dictionary for JSON serialization."""
|
|
52
|
+
return {
|
|
53
|
+
"id": self.id,
|
|
54
|
+
"text": self.text,
|
|
55
|
+
"words": [word.to_dict() for word in self.words],
|
|
56
|
+
"start_time": self.start_time,
|
|
57
|
+
"end_time": self.end_time,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_dict(cls, data: Dict[str, Any]) -> "LyricsSegment":
|
|
62
|
+
"""Create LyricsSegment from dictionary."""
|
|
63
|
+
return cls(
|
|
64
|
+
id=data["id"],
|
|
65
|
+
text=data["text"],
|
|
66
|
+
words=[Word.from_dict(w) for w in data["words"]],
|
|
67
|
+
start_time=data["start_time"],
|
|
68
|
+
end_time=data["end_time"],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class LyricsMetadata:
|
|
74
|
+
"""Standardized metadata for lyrics results."""
|
|
75
|
+
|
|
76
|
+
source: str
|
|
77
|
+
track_name: str
|
|
78
|
+
artist_names: str
|
|
79
|
+
|
|
80
|
+
# Common metadata fields
|
|
81
|
+
album_name: Optional[str] = None
|
|
82
|
+
duration_ms: Optional[int] = None
|
|
83
|
+
explicit: Optional[bool] = None
|
|
84
|
+
language: Optional[str] = None
|
|
85
|
+
is_synced: bool = False
|
|
86
|
+
|
|
87
|
+
# Lyrics provider details
|
|
88
|
+
lyrics_provider: Optional[str] = None
|
|
89
|
+
lyrics_provider_id: Optional[str] = None
|
|
90
|
+
|
|
91
|
+
# Provider-specific metadata
|
|
92
|
+
provider_metadata: Dict[str, Any] = field(default_factory=dict)
|
|
93
|
+
|
|
94
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
95
|
+
"""Convert metadata to dictionary for JSON serialization."""
|
|
96
|
+
return asdict(self)
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_dict(cls, data: Dict[str, Any]) -> "LyricsMetadata":
|
|
100
|
+
"""Create LyricsMetadata from dictionary."""
|
|
101
|
+
return cls(
|
|
102
|
+
source=data["source"],
|
|
103
|
+
track_name=data["track_name"],
|
|
104
|
+
artist_names=data["artist_names"],
|
|
105
|
+
album_name=data.get("album_name"),
|
|
106
|
+
duration_ms=data.get("duration_ms"),
|
|
107
|
+
explicit=data.get("explicit"),
|
|
108
|
+
language=data.get("language"),
|
|
109
|
+
is_synced=data.get("is_synced", False),
|
|
110
|
+
lyrics_provider=data.get("lyrics_provider"),
|
|
111
|
+
lyrics_provider_id=data.get("lyrics_provider_id"),
|
|
112
|
+
provider_metadata=data.get("provider_metadata", {}),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class LyricsData:
|
|
118
|
+
"""Standardized response format for all lyrics providers."""
|
|
119
|
+
|
|
120
|
+
segments: List[LyricsSegment]
|
|
121
|
+
metadata: LyricsMetadata
|
|
122
|
+
source: str # e.g., "genius", "spotify", etc.
|
|
123
|
+
|
|
124
|
+
def get_full_text(self) -> str:
|
|
125
|
+
"""Get the full lyrics text by joining all segment texts."""
|
|
126
|
+
return "\n".join(segment.text for segment in self.segments)
|
|
127
|
+
|
|
128
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
129
|
+
"""Convert result to dictionary for JSON serialization."""
|
|
130
|
+
return {
|
|
131
|
+
"segments": [segment.to_dict() for segment in self.segments],
|
|
132
|
+
"metadata": self.metadata.to_dict(),
|
|
133
|
+
"source": self.source,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def from_dict(cls, data: Dict[str, Any]) -> "LyricsData":
|
|
138
|
+
"""Create LyricsData from dictionary."""
|
|
139
|
+
return cls(
|
|
140
|
+
segments=[LyricsSegment.from_dict(s) for s in data["segments"]],
|
|
141
|
+
metadata=LyricsMetadata.from_dict(data["metadata"]),
|
|
142
|
+
source=data["source"],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass
|
|
147
|
+
class WordCorrection:
|
|
148
|
+
"""Details about a single word correction."""
|
|
149
|
+
|
|
150
|
+
original_word: str
|
|
151
|
+
corrected_word: str # Empty string indicates word should be deleted
|
|
152
|
+
source: str # e.g., "spotify", "genius"
|
|
153
|
+
reason: str # e.g., "matched_in_3_sources", "high_confidence_match"
|
|
154
|
+
original_position: int = 0 # Default to 0 for backwards compatibility with frontend
|
|
155
|
+
segment_index: int = 0 # Default to 0 since it's often not needed
|
|
156
|
+
confidence: Optional[float] = None
|
|
157
|
+
alternatives: Dict[str, int] = field(default_factory=dict) # Other possible corrections and their occurrence counts
|
|
158
|
+
is_deletion: bool = False # New field to explicitly mark deletions
|
|
159
|
+
# New fields for handling word splits
|
|
160
|
+
split_index: Optional[int] = None # Position in the split sequence (0-based)
|
|
161
|
+
split_total: Optional[int] = None # Total number of words in split
|
|
162
|
+
# New field to track position after corrections
|
|
163
|
+
corrected_position: Optional[int] = None
|
|
164
|
+
# New fields to match TypeScript interface
|
|
165
|
+
reference_positions: Optional[Dict[str, int]] = None # Maps source to position in reference text
|
|
166
|
+
length: int = 1 # Default to 1 for single-word corrections
|
|
167
|
+
handler: Optional[str] = None # Name of the correction handler that created this correction
|
|
168
|
+
# New ID fields for tracking word identity through corrections
|
|
169
|
+
word_id: Optional[str] = None # ID of the original word being corrected
|
|
170
|
+
corrected_word_id: Optional[str] = None # ID of the new word after correction
|
|
171
|
+
|
|
172
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
173
|
+
"""Convert to dictionary representation."""
|
|
174
|
+
return asdict(self)
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def from_dict(cls, data: Dict[str, Any]) -> "WordCorrection":
|
|
178
|
+
"""Create WordCorrection from dictionary."""
|
|
179
|
+
# Filter out any keys that aren't part of the dataclass
|
|
180
|
+
valid_fields = {f.name for f in fields(cls)}
|
|
181
|
+
filtered_data = {k: v for k, v in data.items() if k in valid_fields}
|
|
182
|
+
return cls(**filtered_data)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass
|
|
186
|
+
class TranscriptionData:
|
|
187
|
+
"""Structured container for transcription results."""
|
|
188
|
+
|
|
189
|
+
segments: List[LyricsSegment]
|
|
190
|
+
words: List[Word]
|
|
191
|
+
text: str
|
|
192
|
+
source: str # e.g., "whisper", "audioshake"
|
|
193
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
194
|
+
|
|
195
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
196
|
+
"""Convert TranscriptionData to dictionary for JSON serialization."""
|
|
197
|
+
return {
|
|
198
|
+
"segments": [segment.to_dict() for segment in self.segments],
|
|
199
|
+
"words": [word.to_dict() for word in self.words],
|
|
200
|
+
"text": self.text,
|
|
201
|
+
"source": self.source,
|
|
202
|
+
"metadata": self.metadata,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def from_dict(cls, data: Dict[str, Any]) -> "TranscriptionData":
|
|
207
|
+
"""Create TranscriptionData from dictionary."""
|
|
208
|
+
return cls(
|
|
209
|
+
segments=[LyricsSegment.from_dict(s) for s in data["segments"]],
|
|
210
|
+
words=[Word.from_dict(w) for w in data["words"]],
|
|
211
|
+
text=data["text"],
|
|
212
|
+
source=data["source"],
|
|
213
|
+
metadata=data.get("metadata"),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass
|
|
218
|
+
class TranscriptionResult:
|
|
219
|
+
name: str
|
|
220
|
+
priority: int
|
|
221
|
+
result: TranscriptionData
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class PhraseType(Enum):
|
|
225
|
+
"""Types of phrases we can identify"""
|
|
226
|
+
|
|
227
|
+
COMPLETE = "complete" # Grammatically complete unit
|
|
228
|
+
PARTIAL = "partial" # Incomplete but valid fragment
|
|
229
|
+
CROSS_BOUNDARY = "cross" # Crosses natural boundaries
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@dataclass
|
|
233
|
+
class PhraseScore:
|
|
234
|
+
"""Scores for a potential phrase"""
|
|
235
|
+
|
|
236
|
+
phrase_type: PhraseType
|
|
237
|
+
natural_break_score: float # 0-1, how well it respects natural breaks
|
|
238
|
+
length_score: float # 0-1, how appropriate the length is
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def total_score(self) -> float:
|
|
242
|
+
"""Calculate total score with weights"""
|
|
243
|
+
weights = {PhraseType.COMPLETE: 1.0, PhraseType.PARTIAL: 0.7, PhraseType.CROSS_BOUNDARY: 0.3}
|
|
244
|
+
return weights[self.phrase_type] * 0.5 + self.natural_break_score * 0.3 + self.length_score * 0.2
|
|
245
|
+
|
|
246
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
247
|
+
"""Convert PhraseScore to dictionary for JSON serialization."""
|
|
248
|
+
return {
|
|
249
|
+
"phrase_type": self.phrase_type.value, # Convert enum to value for JSON
|
|
250
|
+
"natural_break_score": self.natural_break_score,
|
|
251
|
+
"length_score": self.length_score,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@classmethod
|
|
255
|
+
def from_dict(cls, data: Dict[str, Any]) -> "PhraseScore":
|
|
256
|
+
"""Create PhraseScore from dictionary."""
|
|
257
|
+
return cls(
|
|
258
|
+
phrase_type=PhraseType(data["phrase_type"]), natural_break_score=data["natural_break_score"], length_score=data["length_score"]
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@dataclass
|
|
263
|
+
class AnchorSequence:
|
|
264
|
+
"""Represents a sequence of words that appears in both transcribed and reference lyrics."""
|
|
265
|
+
|
|
266
|
+
id: str # Unique identifier for this anchor sequence
|
|
267
|
+
transcribed_word_ids: List[str] # IDs of Word objects from the transcription
|
|
268
|
+
transcription_position: int # Starting position in transcribed text
|
|
269
|
+
reference_positions: Dict[str, int] # Source -> position mapping
|
|
270
|
+
reference_word_ids: Dict[str, List[str]] # Source -> list of Word IDs from reference
|
|
271
|
+
confidence: float
|
|
272
|
+
|
|
273
|
+
# Backwards compatibility: store original words as text for tests
|
|
274
|
+
_words: Optional[List[str]] = field(default=None, repr=False)
|
|
275
|
+
|
|
276
|
+
def __init__(self, *args, **kwargs):
|
|
277
|
+
"""Backwards-compatible constructor supporting both old and new APIs."""
|
|
278
|
+
# Check for old API usage (either positional args or 'words' keyword)
|
|
279
|
+
if (len(args) >= 3 and isinstance(args[0], list)) or 'words' in kwargs:
|
|
280
|
+
# Old API: either AnchorSequence(words, ...) or AnchorSequence(words=..., ...)
|
|
281
|
+
if 'words' in kwargs:
|
|
282
|
+
# Keyword argument version
|
|
283
|
+
words = kwargs.pop('words')
|
|
284
|
+
transcription_position = kwargs.pop('transcription_position', 0)
|
|
285
|
+
reference_positions = kwargs.pop('reference_positions', {})
|
|
286
|
+
confidence = kwargs.pop('confidence', 0.0)
|
|
287
|
+
else:
|
|
288
|
+
# Positional argument version (may have confidence as keyword)
|
|
289
|
+
words = args[0]
|
|
290
|
+
transcription_position = args[1] if len(args) > 1 else 0
|
|
291
|
+
reference_positions = args[2] if len(args) > 2 else {}
|
|
292
|
+
|
|
293
|
+
# Handle confidence - could be positional or keyword
|
|
294
|
+
if len(args) > 3:
|
|
295
|
+
confidence = args[3]
|
|
296
|
+
else:
|
|
297
|
+
confidence = kwargs.pop('confidence', 0.0)
|
|
298
|
+
|
|
299
|
+
# Store words for backwards compatibility
|
|
300
|
+
self._words = words
|
|
301
|
+
|
|
302
|
+
# Create new API fields
|
|
303
|
+
self.id = kwargs.get('id', WordUtils.generate_id())
|
|
304
|
+
self.transcribed_word_ids = [WordUtils.generate_id() for _ in words]
|
|
305
|
+
self.transcription_position = transcription_position
|
|
306
|
+
self.reference_positions = reference_positions
|
|
307
|
+
# Create reference_word_ids with same structure as reference_positions
|
|
308
|
+
self.reference_word_ids = {source: [WordUtils.generate_id() for _ in words]
|
|
309
|
+
for source in reference_positions.keys()}
|
|
310
|
+
self.confidence = confidence
|
|
311
|
+
else:
|
|
312
|
+
# New API: use keyword arguments
|
|
313
|
+
self.id = kwargs.get('id', args[0] if len(args) > 0 else WordUtils.generate_id())
|
|
314
|
+
self.transcribed_word_ids = kwargs.get('transcribed_word_ids', args[1] if len(args) > 1 else [])
|
|
315
|
+
self.transcription_position = kwargs.get('transcription_position', args[2] if len(args) > 2 else 0)
|
|
316
|
+
self.reference_positions = kwargs.get('reference_positions', args[3] if len(args) > 3 else {})
|
|
317
|
+
self.reference_word_ids = kwargs.get('reference_word_ids', args[4] if len(args) > 4 else {})
|
|
318
|
+
self.confidence = kwargs.get('confidence', args[5] if len(args) > 5 else 0.0)
|
|
319
|
+
self._words = kwargs.get('_words', None)
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def words(self) -> List[str]:
|
|
323
|
+
"""Get the words as a list of strings (backwards compatibility)."""
|
|
324
|
+
if self._words is not None:
|
|
325
|
+
return self._words
|
|
326
|
+
# If we don't have stored words, we can't resolve IDs without a word map
|
|
327
|
+
# This is a limitation of the backwards compatibility
|
|
328
|
+
return [f"word_{i}" for i in range(len(self.transcribed_word_ids))]
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def text(self) -> str:
|
|
332
|
+
"""Get the sequence as a space-separated string."""
|
|
333
|
+
return " ".join(self.words)
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def length(self) -> int:
|
|
337
|
+
"""Get the number of words in the sequence."""
|
|
338
|
+
return len(self.transcribed_word_ids)
|
|
339
|
+
|
|
340
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
341
|
+
"""Convert the anchor sequence to a JSON-serializable dictionary."""
|
|
342
|
+
# Always return the new format that includes all required fields
|
|
343
|
+
result = {
|
|
344
|
+
"id": self.id,
|
|
345
|
+
"transcribed_word_ids": self.transcribed_word_ids,
|
|
346
|
+
"transcription_position": self.transcription_position,
|
|
347
|
+
"reference_positions": self.reference_positions,
|
|
348
|
+
"reference_word_ids": self.reference_word_ids,
|
|
349
|
+
"confidence": self.confidence,
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# For backwards compatibility, include words and text fields if _words is present
|
|
353
|
+
if self._words is not None:
|
|
354
|
+
result.update({
|
|
355
|
+
"words": self._words,
|
|
356
|
+
"text": self.text,
|
|
357
|
+
"length": self.length,
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
return result
|
|
361
|
+
|
|
362
|
+
@classmethod
|
|
363
|
+
def from_dict(cls, data: Dict[str, Any]) -> "AnchorSequence":
|
|
364
|
+
"""Create AnchorSequence from dictionary."""
|
|
365
|
+
# Handle both old and new dictionary formats
|
|
366
|
+
# Check for new format keys FIRST (they take priority even if old keys also present)
|
|
367
|
+
if "transcribed_word_ids" in data:
|
|
368
|
+
# New format - use existing IDs
|
|
369
|
+
return cls(
|
|
370
|
+
id=data.get("id", WordUtils.generate_id()),
|
|
371
|
+
transcribed_word_ids=data["transcribed_word_ids"],
|
|
372
|
+
transcription_position=data["transcription_position"],
|
|
373
|
+
reference_positions=data["reference_positions"],
|
|
374
|
+
reference_word_ids=data["reference_word_ids"],
|
|
375
|
+
confidence=data["confidence"],
|
|
376
|
+
)
|
|
377
|
+
elif "words" in data:
|
|
378
|
+
# Old format only - convert to new format by generating IDs
|
|
379
|
+
# This ensures to_dict() always returns the new format
|
|
380
|
+
words = data["words"]
|
|
381
|
+
return cls(
|
|
382
|
+
id=data.get("id", WordUtils.generate_id()),
|
|
383
|
+
transcribed_word_ids=[WordUtils.generate_id() for _ in words],
|
|
384
|
+
transcription_position=data["transcription_position"],
|
|
385
|
+
reference_positions=data["reference_positions"],
|
|
386
|
+
reference_word_ids={source: [WordUtils.generate_id() for _ in words]
|
|
387
|
+
for source in data["reference_positions"].keys()},
|
|
388
|
+
confidence=data["confidence"],
|
|
389
|
+
)
|
|
390
|
+
else:
|
|
391
|
+
raise ValueError("AnchorSequence.from_dict requires either 'transcribed_word_ids' or 'words' key")
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@dataclass
|
|
395
|
+
class ScoredAnchor:
|
|
396
|
+
"""An anchor sequence with its quality score"""
|
|
397
|
+
|
|
398
|
+
anchor: AnchorSequence
|
|
399
|
+
phrase_score: PhraseScore
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def total_score(self) -> float:
|
|
403
|
+
"""Combine confidence, phrase quality, and length"""
|
|
404
|
+
# Length bonus: (length - 1) * 0.1 gives 0.1 per extra word
|
|
405
|
+
length_bonus = (self.anchor.length - 1) * 0.1
|
|
406
|
+
# Base score heavily weighted towards confidence
|
|
407
|
+
base_score = self.anchor.confidence * 0.8 + self.phrase_score.total_score * 0.2
|
|
408
|
+
# Combine scores
|
|
409
|
+
return base_score + length_bonus
|
|
410
|
+
|
|
411
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
412
|
+
"""Convert the scored anchor to a JSON-serializable dictionary."""
|
|
413
|
+
return {
|
|
414
|
+
**self.anchor.to_dict(),
|
|
415
|
+
"phrase_score": {
|
|
416
|
+
"phrase_type": self.phrase_score.phrase_type.value,
|
|
417
|
+
"natural_break_score": self.phrase_score.natural_break_score,
|
|
418
|
+
"length_score": self.phrase_score.length_score,
|
|
419
|
+
"total_score": self.phrase_score.total_score,
|
|
420
|
+
},
|
|
421
|
+
"total_score": self.total_score,
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
@classmethod
|
|
425
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ScoredAnchor":
|
|
426
|
+
"""Create ScoredAnchor from dictionary."""
|
|
427
|
+
return cls(anchor=AnchorSequence.from_dict(data["anchor"]), phrase_score=PhraseScore.from_dict(data["phrase_score"]))
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@dataclass
|
|
431
|
+
class GapSequence:
|
|
432
|
+
"""Represents a sequence of words between anchor sequences in transcribed lyrics."""
|
|
433
|
+
|
|
434
|
+
id: str # Unique identifier for this gap sequence
|
|
435
|
+
transcribed_word_ids: List[str] # IDs of Word objects from the transcription
|
|
436
|
+
transcription_position: int # Original starting position in transcription
|
|
437
|
+
preceding_anchor_id: Optional[str] # ID of preceding AnchorSequence
|
|
438
|
+
following_anchor_id: Optional[str] # ID of following AnchorSequence
|
|
439
|
+
reference_word_ids: Dict[str, List[str]] # Source -> list of Word IDs from reference
|
|
440
|
+
_corrected_positions: Set[int] = field(default_factory=set, repr=False)
|
|
441
|
+
_position_offset: int = field(default=0, repr=False) # Track cumulative position changes
|
|
442
|
+
|
|
443
|
+
# Backwards compatibility: store original words as text for tests
|
|
444
|
+
_words: Optional[List[str]] = field(default=None, repr=False)
|
|
445
|
+
|
|
446
|
+
def __init__(self, *args, **kwargs):
|
|
447
|
+
"""Backwards-compatible constructor supporting both old and new APIs."""
|
|
448
|
+
if len(args) >= 5 and isinstance(args[0], (list, tuple)):
|
|
449
|
+
# Old API: GapSequence(words, transcription_position, preceding_anchor, following_anchor, reference_words)
|
|
450
|
+
words, transcription_position, preceding_anchor, following_anchor, reference_words = args[:5]
|
|
451
|
+
|
|
452
|
+
# Store words for backwards compatibility
|
|
453
|
+
self._words = list(words) if isinstance(words, tuple) else words
|
|
454
|
+
|
|
455
|
+
# Create new API fields
|
|
456
|
+
self.id = kwargs.get('id', WordUtils.generate_id())
|
|
457
|
+
self.transcribed_word_ids = [WordUtils.generate_id() for _ in self._words]
|
|
458
|
+
self.transcription_position = transcription_position
|
|
459
|
+
self.preceding_anchor_id = getattr(preceding_anchor, 'id', None) if preceding_anchor else None
|
|
460
|
+
self.following_anchor_id = getattr(following_anchor, 'id', None) if following_anchor else None
|
|
461
|
+
# Convert reference_words to reference_word_ids
|
|
462
|
+
self.reference_word_ids = {source: [WordUtils.generate_id() for _ in ref_words]
|
|
463
|
+
for source, ref_words in reference_words.items()}
|
|
464
|
+
self._corrected_positions = set()
|
|
465
|
+
self._position_offset = 0
|
|
466
|
+
else:
|
|
467
|
+
# New API: use keyword arguments
|
|
468
|
+
self.id = kwargs.get('id', args[0] if len(args) > 0 else WordUtils.generate_id())
|
|
469
|
+
self.transcribed_word_ids = kwargs.get('transcribed_word_ids', args[1] if len(args) > 1 else [])
|
|
470
|
+
self.transcription_position = kwargs.get('transcription_position', args[2] if len(args) > 2 else 0)
|
|
471
|
+
self.preceding_anchor_id = kwargs.get('preceding_anchor_id', args[3] if len(args) > 3 else None)
|
|
472
|
+
self.following_anchor_id = kwargs.get('following_anchor_id', args[4] if len(args) > 4 else None)
|
|
473
|
+
self.reference_word_ids = kwargs.get('reference_word_ids', args[5] if len(args) > 5 else {})
|
|
474
|
+
self._corrected_positions = kwargs.get('_corrected_positions', set())
|
|
475
|
+
self._position_offset = kwargs.get('_position_offset', 0)
|
|
476
|
+
self._words = kwargs.get('_words', None)
|
|
477
|
+
|
|
478
|
+
@property
|
|
479
|
+
def words(self) -> List[str]:
|
|
480
|
+
"""Get the words as a list of strings (backwards compatibility)."""
|
|
481
|
+
if self._words is not None:
|
|
482
|
+
return self._words
|
|
483
|
+
# If we don't have stored words, we can't resolve IDs without a word map
|
|
484
|
+
return [f"word_{i}" for i in range(len(self.transcribed_word_ids))]
|
|
485
|
+
|
|
486
|
+
@property
|
|
487
|
+
def text(self) -> str:
|
|
488
|
+
"""Get the sequence as a space-separated string."""
|
|
489
|
+
return " ".join(self.words)
|
|
490
|
+
|
|
491
|
+
@property
|
|
492
|
+
def length(self) -> int:
|
|
493
|
+
"""Get the number of words in the sequence."""
|
|
494
|
+
return len(self.transcribed_word_ids)
|
|
495
|
+
|
|
496
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
497
|
+
"""Convert the gap sequence to a JSON-serializable dictionary."""
|
|
498
|
+
result = {
|
|
499
|
+
"id": self.id,
|
|
500
|
+
"transcribed_word_ids": self.transcribed_word_ids,
|
|
501
|
+
"transcription_position": self.transcription_position,
|
|
502
|
+
"preceding_anchor_id": self.preceding_anchor_id,
|
|
503
|
+
"following_anchor_id": self.following_anchor_id,
|
|
504
|
+
"reference_word_ids": self.reference_word_ids,
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
# For backwards compatibility, include words and text in dict
|
|
508
|
+
if self._words is not None:
|
|
509
|
+
result.update({
|
|
510
|
+
"words": self._words,
|
|
511
|
+
"text": self.text,
|
|
512
|
+
"length": self.length,
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
return result
|
|
516
|
+
|
|
517
|
+
@classmethod
|
|
518
|
+
def from_dict(cls, data: Dict[str, Any]) -> "GapSequence":
|
|
519
|
+
"""Create GapSequence from dictionary."""
|
|
520
|
+
# Handle both old and new dictionary formats
|
|
521
|
+
if "words" in data:
|
|
522
|
+
# Old format - use backwards compatible constructor
|
|
523
|
+
return cls(
|
|
524
|
+
data["words"],
|
|
525
|
+
data["transcription_position"],
|
|
526
|
+
None, # preceding_anchor
|
|
527
|
+
None, # following_anchor
|
|
528
|
+
data.get("reference_words", {}),
|
|
529
|
+
id=data.get("id", WordUtils.generate_id())
|
|
530
|
+
)
|
|
531
|
+
else:
|
|
532
|
+
# New format
|
|
533
|
+
gap = cls(
|
|
534
|
+
id=data.get("id", WordUtils.generate_id()),
|
|
535
|
+
transcribed_word_ids=data["transcribed_word_ids"],
|
|
536
|
+
transcription_position=data["transcription_position"],
|
|
537
|
+
preceding_anchor_id=data["preceding_anchor_id"],
|
|
538
|
+
following_anchor_id=data["following_anchor_id"],
|
|
539
|
+
reference_word_ids=data["reference_word_ids"],
|
|
540
|
+
)
|
|
541
|
+
return gap
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
@dataclass
|
|
545
|
+
class CorrectionStep:
|
|
546
|
+
"""Represents a single correction operation with enough info to replay/undo."""
|
|
547
|
+
|
|
548
|
+
handler_name: str
|
|
549
|
+
affected_word_ids: List[str] # IDs of words modified/deleted
|
|
550
|
+
affected_segment_ids: List[str] # IDs of segments modified
|
|
551
|
+
corrections: List[WordCorrection]
|
|
552
|
+
# State before and after for affected segments
|
|
553
|
+
segments_before: List[LyricsSegment]
|
|
554
|
+
segments_after: List[LyricsSegment]
|
|
555
|
+
# For splits/merges
|
|
556
|
+
created_word_ids: List[str] = field(default_factory=list) # New words created
|
|
557
|
+
deleted_word_ids: List[str] = field(default_factory=list) # Words removed
|
|
558
|
+
|
|
559
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
560
|
+
"""Convert CorrectionStep to dictionary for JSON serialization."""
|
|
561
|
+
return {
|
|
562
|
+
"handler_name": self.handler_name,
|
|
563
|
+
"affected_word_ids": self.affected_word_ids,
|
|
564
|
+
"affected_segment_ids": self.affected_segment_ids,
|
|
565
|
+
"corrections": [c.to_dict() for c in self.corrections],
|
|
566
|
+
"segments_before": [s.to_dict() for s in self.segments_before],
|
|
567
|
+
"segments_after": [s.to_dict() for s in self.segments_after],
|
|
568
|
+
"created_word_ids": self.created_word_ids,
|
|
569
|
+
"deleted_word_ids": self.deleted_word_ids,
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
@classmethod
|
|
573
|
+
def from_dict(cls, data: Dict[str, Any]) -> "CorrectionStep":
|
|
574
|
+
"""Create CorrectionStep from dictionary."""
|
|
575
|
+
return cls(
|
|
576
|
+
handler_name=data["handler_name"],
|
|
577
|
+
affected_word_ids=data["affected_word_ids"],
|
|
578
|
+
affected_segment_ids=data["affected_segment_ids"],
|
|
579
|
+
corrections=[WordCorrection.from_dict(c) for c in data["corrections"]],
|
|
580
|
+
segments_before=[LyricsSegment.from_dict(s) for s in data["segments_before"]],
|
|
581
|
+
segments_after=[LyricsSegment.from_dict(s) for s in data["segments_after"]],
|
|
582
|
+
created_word_ids=data["created_word_ids"],
|
|
583
|
+
deleted_word_ids=data["deleted_word_ids"],
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
@dataclass
|
|
588
|
+
class CorrectionResult:
|
|
589
|
+
"""Container for correction results with detailed correction information."""
|
|
590
|
+
|
|
591
|
+
# Original (uncorrected) data
|
|
592
|
+
original_segments: List[LyricsSegment]
|
|
593
|
+
|
|
594
|
+
# Corrected data
|
|
595
|
+
corrected_segments: List[LyricsSegment]
|
|
596
|
+
|
|
597
|
+
# Correction details
|
|
598
|
+
corrections: List[WordCorrection]
|
|
599
|
+
corrections_made: int
|
|
600
|
+
confidence: float
|
|
601
|
+
|
|
602
|
+
# Debug/analysis information
|
|
603
|
+
reference_lyrics: Dict[str, LyricsData] # Maps source to LyricsData
|
|
604
|
+
anchor_sequences: List[AnchorSequence]
|
|
605
|
+
gap_sequences: List[GapSequence]
|
|
606
|
+
resized_segments: List[LyricsSegment]
|
|
607
|
+
|
|
608
|
+
metadata: Dict[str, Any]
|
|
609
|
+
|
|
610
|
+
# Correction history
|
|
611
|
+
correction_steps: List[CorrectionStep]
|
|
612
|
+
word_id_map: Dict[str, str] # Maps original word IDs to corrected word IDs
|
|
613
|
+
segment_id_map: Dict[str, str] # Maps original segment IDs to corrected segment IDs
|
|
614
|
+
|
|
615
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
616
|
+
"""Convert the correction result to a JSON-serializable dictionary."""
|
|
617
|
+
return {
|
|
618
|
+
"original_segments": [s.to_dict() for s in self.original_segments],
|
|
619
|
+
"reference_lyrics": {source: lyrics.to_dict() for source, lyrics in self.reference_lyrics.items()},
|
|
620
|
+
"anchor_sequences": [a.to_dict() for a in self.anchor_sequences],
|
|
621
|
+
"gap_sequences": [g.to_dict() for g in self.gap_sequences],
|
|
622
|
+
"resized_segments": [s.to_dict() for s in self.resized_segments],
|
|
623
|
+
"corrections_made": self.corrections_made,
|
|
624
|
+
"confidence": self.confidence,
|
|
625
|
+
"corrections": [c.to_dict() for c in self.corrections],
|
|
626
|
+
"corrected_segments": [s.to_dict() for s in self.corrected_segments],
|
|
627
|
+
"metadata": self.metadata,
|
|
628
|
+
"correction_steps": [step.to_dict() for step in self.correction_steps],
|
|
629
|
+
"word_id_map": self.word_id_map,
|
|
630
|
+
"segment_id_map": self.segment_id_map,
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
@classmethod
|
|
634
|
+
def from_dict(cls, data: Dict[str, Any]) -> "CorrectionResult":
|
|
635
|
+
"""Create CorrectionResult from dictionary."""
|
|
636
|
+
return cls(
|
|
637
|
+
original_segments=[LyricsSegment.from_dict(s) for s in data["original_segments"]],
|
|
638
|
+
corrected_segments=[LyricsSegment.from_dict(s) for s in data["corrected_segments"]],
|
|
639
|
+
corrections=[WordCorrection.from_dict(c) for c in data["corrections"]],
|
|
640
|
+
corrections_made=data["corrections_made"],
|
|
641
|
+
confidence=data["confidence"],
|
|
642
|
+
reference_lyrics={source: LyricsData.from_dict(lyrics) for source, lyrics in data["reference_lyrics"].items()},
|
|
643
|
+
anchor_sequences=[AnchorSequence.from_dict(a) for a in data["anchor_sequences"]],
|
|
644
|
+
gap_sequences=[GapSequence.from_dict(g) for g in data["gap_sequences"]],
|
|
645
|
+
resized_segments=[LyricsSegment.from_dict(s) for s in data["resized_segments"]],
|
|
646
|
+
metadata=data["metadata"],
|
|
647
|
+
correction_steps=[CorrectionStep.from_dict(step) for step in data["correction_steps"]],
|
|
648
|
+
word_id_map=data["word_id_map"],
|
|
649
|
+
segment_id_map=data["segment_id_map"],
|
|
650
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import string
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class WordUtils:
|
|
6
|
+
"""Utility class for word-related operations."""
|
|
7
|
+
|
|
8
|
+
_used_ids = set() # Keep track of used IDs
|
|
9
|
+
_id_length = 6 # Length of generated IDs
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def generate_id(cls) -> str:
|
|
13
|
+
"""Generate a unique ID for words/segments.
|
|
14
|
+
|
|
15
|
+
Uses a combination of letters and numbers to create an 8-character ID.
|
|
16
|
+
With 36 possible characters (26 letters + 10 digits), this gives us
|
|
17
|
+
36^8 = ~2.8 trillion possible combinations, which is more than enough
|
|
18
|
+
for our use case while being much shorter than UUID.
|
|
19
|
+
"""
|
|
20
|
+
while True:
|
|
21
|
+
# Generate random string of letters and numbers
|
|
22
|
+
new_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=cls._id_length))
|
|
23
|
+
|
|
24
|
+
# Make sure it's unique for this session
|
|
25
|
+
if new_id not in cls._used_ids:
|
|
26
|
+
cls._used_ids.add(new_id)
|
|
27
|
+
return new_id
|