karaoke-gen 0.57.0__py3-none-any.whl → 0.71.23__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.
Files changed (268) hide show
  1. karaoke_gen/audio_fetcher.py +461 -0
  2. karaoke_gen/audio_processor.py +407 -30
  3. karaoke_gen/config.py +62 -113
  4. karaoke_gen/file_handler.py +32 -59
  5. karaoke_gen/karaoke_finalise/karaoke_finalise.py +148 -67
  6. karaoke_gen/karaoke_gen.py +270 -61
  7. karaoke_gen/lyrics_processor.py +13 -1
  8. karaoke_gen/metadata.py +78 -73
  9. karaoke_gen/pipeline/__init__.py +87 -0
  10. karaoke_gen/pipeline/base.py +215 -0
  11. karaoke_gen/pipeline/context.py +230 -0
  12. karaoke_gen/pipeline/executors/__init__.py +21 -0
  13. karaoke_gen/pipeline/executors/local.py +159 -0
  14. karaoke_gen/pipeline/executors/remote.py +257 -0
  15. karaoke_gen/pipeline/stages/__init__.py +27 -0
  16. karaoke_gen/pipeline/stages/finalize.py +202 -0
  17. karaoke_gen/pipeline/stages/render.py +165 -0
  18. karaoke_gen/pipeline/stages/screens.py +139 -0
  19. karaoke_gen/pipeline/stages/separation.py +191 -0
  20. karaoke_gen/pipeline/stages/transcription.py +191 -0
  21. karaoke_gen/style_loader.py +531 -0
  22. karaoke_gen/utils/bulk_cli.py +6 -0
  23. karaoke_gen/utils/cli_args.py +424 -0
  24. karaoke_gen/utils/gen_cli.py +26 -261
  25. karaoke_gen/utils/remote_cli.py +1815 -0
  26. karaoke_gen/video_background_processor.py +351 -0
  27. karaoke_gen-0.71.23.dist-info/METADATA +610 -0
  28. karaoke_gen-0.71.23.dist-info/RECORD +275 -0
  29. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info}/WHEEL +1 -1
  30. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info}/entry_points.txt +1 -0
  31. lyrics_transcriber/__init__.py +10 -0
  32. lyrics_transcriber/cli/__init__.py +0 -0
  33. lyrics_transcriber/cli/cli_main.py +285 -0
  34. lyrics_transcriber/core/__init__.py +0 -0
  35. lyrics_transcriber/core/config.py +50 -0
  36. lyrics_transcriber/core/controller.py +520 -0
  37. lyrics_transcriber/correction/__init__.py +0 -0
  38. lyrics_transcriber/correction/agentic/__init__.py +9 -0
  39. lyrics_transcriber/correction/agentic/adapter.py +71 -0
  40. lyrics_transcriber/correction/agentic/agent.py +313 -0
  41. lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
  42. lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
  43. lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
  44. lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
  45. lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
  46. lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
  47. lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
  48. lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
  49. lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
  50. lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
  51. lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
  52. lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
  53. lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
  54. lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
  55. lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
  56. lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
  57. lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
  58. lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
  59. lyrics_transcriber/correction/agentic/models/enums.py +38 -0
  60. lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
  61. lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
  62. lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
  63. lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
  64. lyrics_transcriber/correction/agentic/models/utils.py +19 -0
  65. lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
  66. lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
  67. lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
  68. lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
  69. lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
  70. lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
  71. lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
  72. lyrics_transcriber/correction/agentic/providers/base.py +36 -0
  73. lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
  74. lyrics_transcriber/correction/agentic/providers/config.py +73 -0
  75. lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
  76. lyrics_transcriber/correction/agentic/providers/health.py +28 -0
  77. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
  78. lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
  79. lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
  80. lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
  81. lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
  82. lyrics_transcriber/correction/agentic/router.py +35 -0
  83. lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
  84. lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
  85. lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
  86. lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
  87. lyrics_transcriber/correction/anchor_sequence.py +1043 -0
  88. lyrics_transcriber/correction/corrector.py +760 -0
  89. lyrics_transcriber/correction/feedback/__init__.py +2 -0
  90. lyrics_transcriber/correction/feedback/schemas.py +107 -0
  91. lyrics_transcriber/correction/feedback/store.py +236 -0
  92. lyrics_transcriber/correction/handlers/__init__.py +0 -0
  93. lyrics_transcriber/correction/handlers/base.py +52 -0
  94. lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
  95. lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
  96. lyrics_transcriber/correction/handlers/llm.py +293 -0
  97. lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
  98. lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
  99. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
  100. lyrics_transcriber/correction/handlers/repeat.py +88 -0
  101. lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
  102. lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
  103. lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
  104. lyrics_transcriber/correction/handlers/word_operations.py +187 -0
  105. lyrics_transcriber/correction/operations.py +352 -0
  106. lyrics_transcriber/correction/phrase_analyzer.py +435 -0
  107. lyrics_transcriber/correction/text_utils.py +30 -0
  108. lyrics_transcriber/frontend/.gitignore +23 -0
  109. lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
  110. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  111. lyrics_transcriber/frontend/README.md +50 -0
  112. lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
  113. lyrics_transcriber/frontend/__init__.py +25 -0
  114. lyrics_transcriber/frontend/eslint.config.js +28 -0
  115. lyrics_transcriber/frontend/index.html +18 -0
  116. lyrics_transcriber/frontend/package.json +42 -0
  117. lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
  118. lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
  119. lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
  120. lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
  121. lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
  122. lyrics_transcriber/frontend/public/favicon.ico +0 -0
  123. lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
  124. lyrics_transcriber/frontend/src/App.tsx +212 -0
  125. lyrics_transcriber/frontend/src/api.ts +239 -0
  126. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
  127. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
  128. lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
  129. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
  130. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
  131. lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
  132. lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
  133. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
  134. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
  135. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
  136. lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
  137. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
  138. lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
  139. lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
  140. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
  141. lyrics_transcriber/frontend/src/components/Header.tsx +387 -0
  142. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1373 -0
  143. lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
  144. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
  145. lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
  146. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
  147. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
  148. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +688 -0
  149. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
  150. lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
  151. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
  152. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
  153. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
  154. lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
  155. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
  156. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
  157. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
  158. lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
  159. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
  160. lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
  161. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  162. lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
  163. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
  164. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  165. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
  166. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
  167. lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
  168. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  169. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
  170. lyrics_transcriber/frontend/src/main.tsx +17 -0
  171. lyrics_transcriber/frontend/src/theme.ts +177 -0
  172. lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
  173. lyrics_transcriber/frontend/src/types.js +2 -0
  174. lyrics_transcriber/frontend/src/types.ts +199 -0
  175. lyrics_transcriber/frontend/src/validation.ts +132 -0
  176. lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
  177. lyrics_transcriber/frontend/tsconfig.app.json +26 -0
  178. lyrics_transcriber/frontend/tsconfig.json +25 -0
  179. lyrics_transcriber/frontend/tsconfig.node.json +23 -0
  180. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
  181. lyrics_transcriber/frontend/update_version.js +11 -0
  182. lyrics_transcriber/frontend/vite.config.d.ts +2 -0
  183. lyrics_transcriber/frontend/vite.config.js +10 -0
  184. lyrics_transcriber/frontend/vite.config.ts +11 -0
  185. lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
  186. lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
  187. lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
  188. lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js +42039 -0
  189. lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js.map +1 -0
  190. lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
  191. lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
  192. lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
  193. lyrics_transcriber/frontend/web_assets/index.html +18 -0
  194. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
  195. lyrics_transcriber/frontend/yarn.lock +3752 -0
  196. lyrics_transcriber/lyrics/__init__.py +0 -0
  197. lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
  198. lyrics_transcriber/lyrics/file_provider.py +95 -0
  199. lyrics_transcriber/lyrics/genius.py +384 -0
  200. lyrics_transcriber/lyrics/lrclib.py +231 -0
  201. lyrics_transcriber/lyrics/musixmatch.py +156 -0
  202. lyrics_transcriber/lyrics/spotify.py +290 -0
  203. lyrics_transcriber/lyrics/user_input_provider.py +44 -0
  204. lyrics_transcriber/output/__init__.py +0 -0
  205. lyrics_transcriber/output/ass/__init__.py +21 -0
  206. lyrics_transcriber/output/ass/ass.py +2088 -0
  207. lyrics_transcriber/output/ass/ass_specs.txt +732 -0
  208. lyrics_transcriber/output/ass/config.py +180 -0
  209. lyrics_transcriber/output/ass/constants.py +23 -0
  210. lyrics_transcriber/output/ass/event.py +94 -0
  211. lyrics_transcriber/output/ass/formatters.py +132 -0
  212. lyrics_transcriber/output/ass/lyrics_line.py +265 -0
  213. lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
  214. lyrics_transcriber/output/ass/section_detector.py +89 -0
  215. lyrics_transcriber/output/ass/section_screen.py +106 -0
  216. lyrics_transcriber/output/ass/style.py +187 -0
  217. lyrics_transcriber/output/cdg.py +619 -0
  218. lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
  219. lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
  220. lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
  221. lyrics_transcriber/output/cdgmaker/config.py +151 -0
  222. lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
  223. lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
  224. lyrics_transcriber/output/cdgmaker/pack.py +507 -0
  225. lyrics_transcriber/output/cdgmaker/render.py +346 -0
  226. lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
  227. lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
  228. lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
  229. lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
  230. lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
  231. lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
  232. lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
  233. lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
  234. lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
  235. lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
  236. lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
  237. lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
  238. lyrics_transcriber/output/cdgmaker/utils.py +132 -0
  239. lyrics_transcriber/output/countdown_processor.py +267 -0
  240. lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
  241. lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
  242. lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
  243. lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
  244. lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
  245. lyrics_transcriber/output/fonts/arial.ttf +0 -0
  246. lyrics_transcriber/output/fonts/georgia.ttf +0 -0
  247. lyrics_transcriber/output/fonts/verdana.ttf +0 -0
  248. lyrics_transcriber/output/generator.py +257 -0
  249. lyrics_transcriber/output/lrc_to_cdg.py +61 -0
  250. lyrics_transcriber/output/lyrics_file.py +102 -0
  251. lyrics_transcriber/output/plain_text.py +96 -0
  252. lyrics_transcriber/output/segment_resizer.py +431 -0
  253. lyrics_transcriber/output/subtitles.py +397 -0
  254. lyrics_transcriber/output/video.py +544 -0
  255. lyrics_transcriber/review/__init__.py +0 -0
  256. lyrics_transcriber/review/server.py +676 -0
  257. lyrics_transcriber/storage/__init__.py +0 -0
  258. lyrics_transcriber/storage/dropbox.py +225 -0
  259. lyrics_transcriber/transcribers/__init__.py +0 -0
  260. lyrics_transcriber/transcribers/audioshake.py +290 -0
  261. lyrics_transcriber/transcribers/base_transcriber.py +157 -0
  262. lyrics_transcriber/transcribers/whisper.py +330 -0
  263. lyrics_transcriber/types.py +648 -0
  264. lyrics_transcriber/utils/__init__.py +0 -0
  265. lyrics_transcriber/utils/word_utils.py +27 -0
  266. karaoke_gen-0.57.0.dist-info/METADATA +0 -167
  267. karaoke_gen-0.57.0.dist-info/RECORD +0 -23
  268. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,648 @@
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
+ if "words" in data:
367
+ # Old format - convert to new format without setting _words
368
+ # This ensures to_dict() always returns the new format
369
+ words = data["words"]
370
+ return cls(
371
+ id=data.get("id", WordUtils.generate_id()),
372
+ transcribed_word_ids=[WordUtils.generate_id() for _ in words],
373
+ transcription_position=data["transcription_position"],
374
+ reference_positions=data["reference_positions"],
375
+ reference_word_ids={source: [WordUtils.generate_id() for _ in words]
376
+ for source in data["reference_positions"].keys()},
377
+ confidence=data["confidence"],
378
+ # Don't set _words - this ensures we always use the new format
379
+ )
380
+ else:
381
+ # New format
382
+ return cls(
383
+ id=data.get("id", WordUtils.generate_id()),
384
+ transcribed_word_ids=data["transcribed_word_ids"],
385
+ transcription_position=data["transcription_position"],
386
+ reference_positions=data["reference_positions"],
387
+ reference_word_ids=data["reference_word_ids"],
388
+ confidence=data["confidence"],
389
+ )
390
+
391
+
392
+ @dataclass
393
+ class ScoredAnchor:
394
+ """An anchor sequence with its quality score"""
395
+
396
+ anchor: AnchorSequence
397
+ phrase_score: PhraseScore
398
+
399
+ @property
400
+ def total_score(self) -> float:
401
+ """Combine confidence, phrase quality, and length"""
402
+ # Length bonus: (length - 1) * 0.1 gives 0.1 per extra word
403
+ length_bonus = (self.anchor.length - 1) * 0.1
404
+ # Base score heavily weighted towards confidence
405
+ base_score = self.anchor.confidence * 0.8 + self.phrase_score.total_score * 0.2
406
+ # Combine scores
407
+ return base_score + length_bonus
408
+
409
+ def to_dict(self) -> Dict[str, Any]:
410
+ """Convert the scored anchor to a JSON-serializable dictionary."""
411
+ return {
412
+ **self.anchor.to_dict(),
413
+ "phrase_score": {
414
+ "phrase_type": self.phrase_score.phrase_type.value,
415
+ "natural_break_score": self.phrase_score.natural_break_score,
416
+ "length_score": self.phrase_score.length_score,
417
+ "total_score": self.phrase_score.total_score,
418
+ },
419
+ "total_score": self.total_score,
420
+ }
421
+
422
+ @classmethod
423
+ def from_dict(cls, data: Dict[str, Any]) -> "ScoredAnchor":
424
+ """Create ScoredAnchor from dictionary."""
425
+ return cls(anchor=AnchorSequence.from_dict(data["anchor"]), phrase_score=PhraseScore.from_dict(data["phrase_score"]))
426
+
427
+
428
+ @dataclass
429
+ class GapSequence:
430
+ """Represents a sequence of words between anchor sequences in transcribed lyrics."""
431
+
432
+ id: str # Unique identifier for this gap sequence
433
+ transcribed_word_ids: List[str] # IDs of Word objects from the transcription
434
+ transcription_position: int # Original starting position in transcription
435
+ preceding_anchor_id: Optional[str] # ID of preceding AnchorSequence
436
+ following_anchor_id: Optional[str] # ID of following AnchorSequence
437
+ reference_word_ids: Dict[str, List[str]] # Source -> list of Word IDs from reference
438
+ _corrected_positions: Set[int] = field(default_factory=set, repr=False)
439
+ _position_offset: int = field(default=0, repr=False) # Track cumulative position changes
440
+
441
+ # Backwards compatibility: store original words as text for tests
442
+ _words: Optional[List[str]] = field(default=None, repr=False)
443
+
444
+ def __init__(self, *args, **kwargs):
445
+ """Backwards-compatible constructor supporting both old and new APIs."""
446
+ if len(args) >= 5 and isinstance(args[0], (list, tuple)):
447
+ # Old API: GapSequence(words, transcription_position, preceding_anchor, following_anchor, reference_words)
448
+ words, transcription_position, preceding_anchor, following_anchor, reference_words = args[:5]
449
+
450
+ # Store words for backwards compatibility
451
+ self._words = list(words) if isinstance(words, tuple) else words
452
+
453
+ # Create new API fields
454
+ self.id = kwargs.get('id', WordUtils.generate_id())
455
+ self.transcribed_word_ids = [WordUtils.generate_id() for _ in self._words]
456
+ self.transcription_position = transcription_position
457
+ self.preceding_anchor_id = getattr(preceding_anchor, 'id', None) if preceding_anchor else None
458
+ self.following_anchor_id = getattr(following_anchor, 'id', None) if following_anchor else None
459
+ # Convert reference_words to reference_word_ids
460
+ self.reference_word_ids = {source: [WordUtils.generate_id() for _ in ref_words]
461
+ for source, ref_words in reference_words.items()}
462
+ self._corrected_positions = set()
463
+ self._position_offset = 0
464
+ else:
465
+ # New API: use keyword arguments
466
+ self.id = kwargs.get('id', args[0] if len(args) > 0 else WordUtils.generate_id())
467
+ self.transcribed_word_ids = kwargs.get('transcribed_word_ids', args[1] if len(args) > 1 else [])
468
+ self.transcription_position = kwargs.get('transcription_position', args[2] if len(args) > 2 else 0)
469
+ self.preceding_anchor_id = kwargs.get('preceding_anchor_id', args[3] if len(args) > 3 else None)
470
+ self.following_anchor_id = kwargs.get('following_anchor_id', args[4] if len(args) > 4 else None)
471
+ self.reference_word_ids = kwargs.get('reference_word_ids', args[5] if len(args) > 5 else {})
472
+ self._corrected_positions = kwargs.get('_corrected_positions', set())
473
+ self._position_offset = kwargs.get('_position_offset', 0)
474
+ self._words = kwargs.get('_words', None)
475
+
476
+ @property
477
+ def words(self) -> List[str]:
478
+ """Get the words as a list of strings (backwards compatibility)."""
479
+ if self._words is not None:
480
+ return self._words
481
+ # If we don't have stored words, we can't resolve IDs without a word map
482
+ return [f"word_{i}" for i in range(len(self.transcribed_word_ids))]
483
+
484
+ @property
485
+ def text(self) -> str:
486
+ """Get the sequence as a space-separated string."""
487
+ return " ".join(self.words)
488
+
489
+ @property
490
+ def length(self) -> int:
491
+ """Get the number of words in the sequence."""
492
+ return len(self.transcribed_word_ids)
493
+
494
+ def to_dict(self) -> Dict[str, Any]:
495
+ """Convert the gap sequence to a JSON-serializable dictionary."""
496
+ result = {
497
+ "id": self.id,
498
+ "transcribed_word_ids": self.transcribed_word_ids,
499
+ "transcription_position": self.transcription_position,
500
+ "preceding_anchor_id": self.preceding_anchor_id,
501
+ "following_anchor_id": self.following_anchor_id,
502
+ "reference_word_ids": self.reference_word_ids,
503
+ }
504
+
505
+ # For backwards compatibility, include words and text in dict
506
+ if self._words is not None:
507
+ result.update({
508
+ "words": self._words,
509
+ "text": self.text,
510
+ "length": self.length,
511
+ })
512
+
513
+ return result
514
+
515
+ @classmethod
516
+ def from_dict(cls, data: Dict[str, Any]) -> "GapSequence":
517
+ """Create GapSequence from dictionary."""
518
+ # Handle both old and new dictionary formats
519
+ if "words" in data:
520
+ # Old format - use backwards compatible constructor
521
+ return cls(
522
+ data["words"],
523
+ data["transcription_position"],
524
+ None, # preceding_anchor
525
+ None, # following_anchor
526
+ data.get("reference_words", {}),
527
+ id=data.get("id", WordUtils.generate_id())
528
+ )
529
+ else:
530
+ # New format
531
+ gap = cls(
532
+ id=data.get("id", WordUtils.generate_id()),
533
+ transcribed_word_ids=data["transcribed_word_ids"],
534
+ transcription_position=data["transcription_position"],
535
+ preceding_anchor_id=data["preceding_anchor_id"],
536
+ following_anchor_id=data["following_anchor_id"],
537
+ reference_word_ids=data["reference_word_ids"],
538
+ )
539
+ return gap
540
+
541
+
542
+ @dataclass
543
+ class CorrectionStep:
544
+ """Represents a single correction operation with enough info to replay/undo."""
545
+
546
+ handler_name: str
547
+ affected_word_ids: List[str] # IDs of words modified/deleted
548
+ affected_segment_ids: List[str] # IDs of segments modified
549
+ corrections: List[WordCorrection]
550
+ # State before and after for affected segments
551
+ segments_before: List[LyricsSegment]
552
+ segments_after: List[LyricsSegment]
553
+ # For splits/merges
554
+ created_word_ids: List[str] = field(default_factory=list) # New words created
555
+ deleted_word_ids: List[str] = field(default_factory=list) # Words removed
556
+
557
+ def to_dict(self) -> Dict[str, Any]:
558
+ """Convert CorrectionStep to dictionary for JSON serialization."""
559
+ return {
560
+ "handler_name": self.handler_name,
561
+ "affected_word_ids": self.affected_word_ids,
562
+ "affected_segment_ids": self.affected_segment_ids,
563
+ "corrections": [c.to_dict() for c in self.corrections],
564
+ "segments_before": [s.to_dict() for s in self.segments_before],
565
+ "segments_after": [s.to_dict() for s in self.segments_after],
566
+ "created_word_ids": self.created_word_ids,
567
+ "deleted_word_ids": self.deleted_word_ids,
568
+ }
569
+
570
+ @classmethod
571
+ def from_dict(cls, data: Dict[str, Any]) -> "CorrectionStep":
572
+ """Create CorrectionStep from dictionary."""
573
+ return cls(
574
+ handler_name=data["handler_name"],
575
+ affected_word_ids=data["affected_word_ids"],
576
+ affected_segment_ids=data["affected_segment_ids"],
577
+ corrections=[WordCorrection.from_dict(c) for c in data["corrections"]],
578
+ segments_before=[LyricsSegment.from_dict(s) for s in data["segments_before"]],
579
+ segments_after=[LyricsSegment.from_dict(s) for s in data["segments_after"]],
580
+ created_word_ids=data["created_word_ids"],
581
+ deleted_word_ids=data["deleted_word_ids"],
582
+ )
583
+
584
+
585
+ @dataclass
586
+ class CorrectionResult:
587
+ """Container for correction results with detailed correction information."""
588
+
589
+ # Original (uncorrected) data
590
+ original_segments: List[LyricsSegment]
591
+
592
+ # Corrected data
593
+ corrected_segments: List[LyricsSegment]
594
+
595
+ # Correction details
596
+ corrections: List[WordCorrection]
597
+ corrections_made: int
598
+ confidence: float
599
+
600
+ # Debug/analysis information
601
+ reference_lyrics: Dict[str, LyricsData] # Maps source to LyricsData
602
+ anchor_sequences: List[AnchorSequence]
603
+ gap_sequences: List[GapSequence]
604
+ resized_segments: List[LyricsSegment]
605
+
606
+ metadata: Dict[str, Any]
607
+
608
+ # Correction history
609
+ correction_steps: List[CorrectionStep]
610
+ word_id_map: Dict[str, str] # Maps original word IDs to corrected word IDs
611
+ segment_id_map: Dict[str, str] # Maps original segment IDs to corrected segment IDs
612
+
613
+ def to_dict(self) -> Dict[str, Any]:
614
+ """Convert the correction result to a JSON-serializable dictionary."""
615
+ return {
616
+ "original_segments": [s.to_dict() for s in self.original_segments],
617
+ "reference_lyrics": {source: lyrics.to_dict() for source, lyrics in self.reference_lyrics.items()},
618
+ "anchor_sequences": [a.to_dict() for a in self.anchor_sequences],
619
+ "gap_sequences": [g.to_dict() for g in self.gap_sequences],
620
+ "resized_segments": [s.to_dict() for s in self.resized_segments],
621
+ "corrections_made": self.corrections_made,
622
+ "confidence": self.confidence,
623
+ "corrections": [c.to_dict() for c in self.corrections],
624
+ "corrected_segments": [s.to_dict() for s in self.corrected_segments],
625
+ "metadata": self.metadata,
626
+ "correction_steps": [step.to_dict() for step in self.correction_steps],
627
+ "word_id_map": self.word_id_map,
628
+ "segment_id_map": self.segment_id_map,
629
+ }
630
+
631
+ @classmethod
632
+ def from_dict(cls, data: Dict[str, Any]) -> "CorrectionResult":
633
+ """Create CorrectionResult from dictionary."""
634
+ return cls(
635
+ original_segments=[LyricsSegment.from_dict(s) for s in data["original_segments"]],
636
+ corrected_segments=[LyricsSegment.from_dict(s) for s in data["corrected_segments"]],
637
+ corrections=[WordCorrection.from_dict(c) for c in data["corrections"]],
638
+ corrections_made=data["corrections_made"],
639
+ confidence=data["confidence"],
640
+ reference_lyrics={source: LyricsData.from_dict(lyrics) for source, lyrics in data["reference_lyrics"].items()},
641
+ anchor_sequences=[AnchorSequence.from_dict(a) for a in data["anchor_sequences"]],
642
+ gap_sequences=[GapSequence.from_dict(g) for g in data["gap_sequences"]],
643
+ resized_segments=[LyricsSegment.from_dict(s) for s in data["resized_segments"]],
644
+ metadata=data["metadata"],
645
+ correction_steps=[CorrectionStep.from_dict(step) for step in data["correction_steps"]],
646
+ word_id_map=data["word_id_map"],
647
+ segment_id_map=data["segment_id_map"],
648
+ )
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