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,156 @@
1
+ import logging
2
+ from typing import Optional, Dict, Any
3
+ import requests
4
+ from lyrics_transcriber.types import LyricsData, LyricsMetadata
5
+ from lyrics_transcriber.lyrics.base_lyrics_provider import BaseLyricsProvider, LyricsProviderConfig
6
+
7
+
8
+ class MusixmatchProvider(BaseLyricsProvider):
9
+ """Handles fetching lyrics from Musixmatch via RapidAPI."""
10
+
11
+ def __init__(self, config: LyricsProviderConfig, logger: Optional[logging.Logger] = None):
12
+ super().__init__(config, logger)
13
+ self.rapidapi_key = config.rapidapi_key
14
+
15
+ def _fetch_data_from_source(self, artist: str, title: str) -> Optional[Dict[str, Any]]:
16
+ """Fetch raw song data from Musixmatch via RapidAPI."""
17
+ if not self.rapidapi_key:
18
+ self.logger.warning("No RapidAPI key provided for Musixmatch")
19
+ return None
20
+
21
+ self.logger.info(f"Fetching lyrics from Musixmatch for {artist} - {title}")
22
+
23
+ try:
24
+ # Construct the API URL with artist and title
25
+ url = f"https://musixmatch-song-lyrics-api.p.rapidapi.com/lyrics/{artist}/{title}/"
26
+
27
+ headers = {
28
+ "x-rapidapi-key": self.rapidapi_key,
29
+ "x-rapidapi-host": "musixmatch-song-lyrics-api.p.rapidapi.com"
30
+ }
31
+
32
+ self.logger.debug(f"Making Musixmatch API request to: {url}")
33
+ response = requests.get(url, headers=headers, timeout=10)
34
+ response.raise_for_status()
35
+
36
+ data = response.json()
37
+
38
+ # Check if we got a valid response
39
+ if not data.get("message", {}).get("body", {}).get("macro_calls"):
40
+ self.logger.warning("Invalid response structure from Musixmatch API")
41
+ return None
42
+
43
+ # Check if lyrics are available
44
+ lyrics_data = data.get("message", {}).get("body", {}).get("macro_calls", {}).get("track.lyrics.get", {})
45
+ if not lyrics_data.get("message", {}).get("body", {}).get("lyrics"):
46
+ self.logger.warning("No lyrics found in Musixmatch response")
47
+ return None
48
+
49
+ self.logger.info("Successfully fetched lyrics from Musixmatch")
50
+ return data
51
+
52
+ except requests.exceptions.RequestException as e:
53
+ self.logger.error(f"Musixmatch API request failed: {str(e)}")
54
+ return None
55
+ except Exception as e:
56
+ self.logger.error(f"Error fetching from Musixmatch: {str(e)}")
57
+ return None
58
+
59
+ def _convert_result_format(self, raw_data: Dict[str, Any]) -> LyricsData:
60
+ """Convert Musixmatch's raw API response to standardized format."""
61
+ try:
62
+ # Extract macro calls from the nested response
63
+ macro_calls = raw_data.get("message", {}).get("body", {}).get("macro_calls", {})
64
+
65
+ # Extract track information
66
+ track_data = macro_calls.get("matcher.track.get", {}).get("message", {}).get("body", {}).get("track", {})
67
+
68
+ # Extract lyrics information
69
+ lyrics_data = macro_calls.get("track.lyrics.get", {}).get("message", {}).get("body", {}).get("lyrics", {})
70
+
71
+ # Get the actual lyrics text
72
+ lyrics_text = lyrics_data.get("lyrics_body", "")
73
+
74
+ # Clean the lyrics
75
+ lyrics_text = self._clean_lyrics(lyrics_text)
76
+
77
+ # Create metadata object
78
+ metadata = LyricsMetadata(
79
+ source="musixmatch",
80
+ track_name=track_data.get("track_name", ""),
81
+ artist_names=track_data.get("artist_name", ""),
82
+ album_name=track_data.get("album_name", ""),
83
+ duration_ms=track_data.get("track_length", 0) * 1000 if track_data.get("track_length") else None,
84
+ explicit=bool(track_data.get("explicit", 0)),
85
+ language=lyrics_data.get("lyrics_language", ""),
86
+ is_synced=False, # Musixmatch API doesn't provide sync data in this format
87
+ lyrics_provider="musixmatch",
88
+ lyrics_provider_id=str(lyrics_data.get("lyrics_id", "")),
89
+ provider_metadata={
90
+ "musixmatch_track_id": track_data.get("track_id"),
91
+ "musixmatch_lyrics_id": lyrics_data.get("lyrics_id"),
92
+ "album_id": track_data.get("album_id"),
93
+ "artist_id": track_data.get("artist_id"),
94
+ "track_share_url": track_data.get("track_share_url"),
95
+ "track_edit_url": track_data.get("track_edit_url"),
96
+ "lyrics_language": lyrics_data.get("lyrics_language"),
97
+ "lyrics_language_description": lyrics_data.get("lyrics_language_description"),
98
+ "lyrics_copyright": lyrics_data.get("lyrics_copyright"),
99
+ "track_rating": track_data.get("track_rating"),
100
+ "num_favourite": track_data.get("num_favourite"),
101
+ "first_release_date": track_data.get("first_release_date"),
102
+ "spotify_id": track_data.get("track_spotify_id"),
103
+ "isrc": track_data.get("track_isrc"),
104
+ "api_source": "rapidapi_musixmatch",
105
+ },
106
+ )
107
+
108
+ # Create segments with words from lyrics
109
+ segments = self._create_segments_with_words(lyrics_text, is_synced=False)
110
+
111
+ # Create result object with segments
112
+ return LyricsData(source="musixmatch", segments=segments, metadata=metadata)
113
+
114
+ except Exception as e:
115
+ self.logger.error(f"Error converting Musixmatch response format: {str(e)}")
116
+ # Return empty lyrics data if conversion fails
117
+ return LyricsData(
118
+ source="musixmatch",
119
+ segments=[],
120
+ metadata=LyricsMetadata(
121
+ source="musixmatch",
122
+ track_name="",
123
+ artist_names="",
124
+ lyrics_provider="musixmatch",
125
+ is_synced=False,
126
+ provider_metadata={"api_source": "rapidapi_musixmatch", "conversion_error": str(e)},
127
+ )
128
+ )
129
+
130
+ def _clean_lyrics(self, lyrics: str) -> str:
131
+ """Clean and process lyrics from Musixmatch to remove unwanted content."""
132
+ if not isinstance(lyrics, str):
133
+ self.logger.warning(f"Expected string for lyrics, got {type(lyrics)}: {repr(lyrics)}")
134
+ if lyrics is None:
135
+ return ""
136
+ try:
137
+ lyrics = str(lyrics)
138
+ except Exception as e:
139
+ self.logger.error(f"Failed to convert lyrics to string: {e}")
140
+ return ""
141
+
142
+ # Replace escaped newlines with actual newlines, handling whitespace
143
+ import re
144
+ lyrics = re.sub(r'\s*\\n\s*', '\n', lyrics)
145
+
146
+ # Remove any HTML tags that might be present
147
+ lyrics = re.sub(r'<[^>]+>', '', lyrics)
148
+
149
+ # Clean up multiple consecutive newlines
150
+ lyrics = re.sub(r'\n\s*\n\s*\n+', '\n\n', lyrics)
151
+
152
+ # Clean up leading/trailing whitespace
153
+ lyrics = lyrics.strip()
154
+
155
+ self.logger.debug("Completed Musixmatch lyrics cleaning process")
156
+ return lyrics
@@ -0,0 +1,290 @@
1
+ import logging
2
+ from typing import Optional, Dict, Any
3
+ import syrics.api
4
+ import time
5
+ import requests
6
+
7
+ from lyrics_transcriber.types import LyricsData, LyricsMetadata, LyricsSegment, Word
8
+ from lyrics_transcriber.lyrics.base_lyrics_provider import BaseLyricsProvider, LyricsProviderConfig
9
+ from lyrics_transcriber.utils.word_utils import WordUtils
10
+
11
+
12
+ class SpotifyProvider(BaseLyricsProvider):
13
+ """Handles fetching lyrics from Spotify."""
14
+
15
+ def __init__(self, config: LyricsProviderConfig, logger: Optional[logging.Logger] = None):
16
+ super().__init__(config, logger)
17
+ self.cookie = config.spotify_cookie
18
+ self.rapidapi_key = config.rapidapi_key
19
+ self.client = None
20
+
21
+ # Only initialize syrics client if rapidapi_key is not set
22
+ if self.cookie and not self.rapidapi_key:
23
+ max_retries = 5
24
+ retry_delay = 5 # seconds
25
+
26
+ for attempt in range(max_retries):
27
+ try:
28
+ self.client = syrics.api.Spotify(self.cookie)
29
+ break # Successfully initialized
30
+ except Exception as e:
31
+ if attempt == max_retries - 1: # Last attempt
32
+ self.logger.error(f"Failed to initialize Spotify client after {max_retries} attempts: {str(e)}")
33
+ break
34
+ self.logger.warning(f"Attempt {attempt + 1}/{max_retries} failed, retrying in {retry_delay} seconds...")
35
+ time.sleep(retry_delay)
36
+
37
+ def _fetch_data_from_source(self, artist: str, title: str) -> Optional[Dict[str, Any]]:
38
+ """Fetch raw data from Spotify APIs using RapidAPI or syrics library."""
39
+ # Try RapidAPI first if available
40
+ if self.rapidapi_key:
41
+ self.logger.info(f"Trying RapidAPI for {artist} - {title}")
42
+ result = self._fetch_from_rapidapi(artist, title)
43
+ if result:
44
+ return result
45
+
46
+ # Fall back to syrics library
47
+ if not self.client:
48
+ self.logger.warning("No Spotify cookie provided and RapidAPI failed")
49
+ return None
50
+
51
+ try:
52
+ # Search for track
53
+ search_query = f"{title} - {artist}"
54
+ search_results = self.client.search(search_query, type="track", limit=1)
55
+
56
+ track_data = search_results["tracks"]["items"][0]
57
+ self.logger.debug(
58
+ f"Found track: {track_data['artists'][0]['name']} - {track_data['name']} " f"({track_data['external_urls']['spotify']})"
59
+ )
60
+
61
+ # Get lyrics data
62
+ lyrics_data = self.client.get_lyrics(track_data["id"])
63
+ if not lyrics_data:
64
+ return None
65
+
66
+ return {"track_data": track_data, "lyrics_data": lyrics_data}
67
+ except Exception as e:
68
+ self.logger.error(f"Error fetching from Spotify: {str(e)}")
69
+ return None
70
+
71
+ def _fetch_from_rapidapi(self, artist: str, title: str) -> Optional[Dict[str, Any]]:
72
+ """Fetch song data using RapidAPI."""
73
+ try:
74
+ # Step 1: Search for the track
75
+ search_url = "https://spotify-scraper.p.rapidapi.com/v1/track/search"
76
+ search_params = {
77
+ "name": f"{title} {artist}"
78
+ }
79
+
80
+ headers = {
81
+ "x-rapidapi-key": self.rapidapi_key,
82
+ "x-rapidapi-host": "spotify-scraper.p.rapidapi.com"
83
+ }
84
+
85
+ self.logger.debug(f"Making RapidAPI search request for '{artist} {title}'")
86
+ search_response = requests.get(search_url, headers=headers, params=search_params, timeout=10)
87
+ search_response.raise_for_status()
88
+
89
+ search_data = search_response.json()
90
+
91
+ # Check if search was successful
92
+ if not search_data.get("status") or search_data.get("errorId") != "Success":
93
+ self.logger.warning("RapidAPI search failed")
94
+ return None
95
+
96
+ track_id = search_data.get("id")
97
+ if not track_id:
98
+ self.logger.warning("No track ID found in RapidAPI search results")
99
+ return None
100
+
101
+ self.logger.debug(f"Found track ID: {track_id}")
102
+
103
+ # Step 2: Fetch lyrics using the track ID
104
+ lyrics_url = "https://spotify-scraper.p.rapidapi.com/v1/track/lyrics"
105
+ lyrics_params = {
106
+ "trackId": track_id,
107
+ "format": "json",
108
+ "removeNote": "true"
109
+ }
110
+
111
+ self.logger.debug(f"Making RapidAPI lyrics request for track ID {track_id}")
112
+ lyrics_response = requests.get(lyrics_url, headers=headers, params=lyrics_params, timeout=10)
113
+ lyrics_response.raise_for_status()
114
+
115
+ lyrics_data = lyrics_response.json()
116
+
117
+ # Create a clean RapidAPI response structure
118
+ rapidapi_response = {
119
+ "track_data": search_data,
120
+ "lyrics_data": lyrics_data,
121
+ # Mark this as RapidAPI source
122
+ "_rapidapi_source": True
123
+ }
124
+
125
+ self.logger.info("Successfully fetched lyrics from RapidAPI")
126
+ return rapidapi_response
127
+
128
+ except requests.exceptions.RequestException as e:
129
+ self.logger.error(f"RapidAPI request failed: {str(e)}")
130
+ return None
131
+ except Exception as e:
132
+ self.logger.error(f"Error fetching from RapidAPI: {str(e)}")
133
+ return None
134
+
135
+ def _convert_result_format(self, raw_data: Dict[str, Any]) -> LyricsData:
136
+ """Convert Spotify's raw API response to standardized format."""
137
+ # Use our explicit source marker for detection
138
+ is_rapidapi = raw_data.get("_rapidapi_source", False)
139
+
140
+ if is_rapidapi:
141
+ return self._convert_rapidapi_format(raw_data)
142
+ else:
143
+ return self._convert_syrics_format(raw_data)
144
+
145
+ def _convert_syrics_format(self, raw_data: Dict[str, Any]) -> LyricsData:
146
+ """Convert syrics format to standardized format."""
147
+ track_data = raw_data["track_data"]
148
+ lyrics_data = raw_data["lyrics_data"]["lyrics"]
149
+
150
+ # Create metadata object
151
+ metadata = LyricsMetadata(
152
+ source="spotify",
153
+ track_name=track_data.get("name"),
154
+ artist_names=", ".join(artist.get("name", "") for artist in track_data.get("artists", [])),
155
+ album_name=track_data.get("album", {}).get("name"),
156
+ duration_ms=track_data.get("duration_ms"),
157
+ explicit=track_data.get("explicit"),
158
+ language=lyrics_data.get("language"),
159
+ is_synced=lyrics_data.get("syncType") == "LINE_SYNCED",
160
+ lyrics_provider=lyrics_data.get("provider"),
161
+ lyrics_provider_id=lyrics_data.get("providerLyricsId"),
162
+ provider_metadata={
163
+ "spotify_id": track_data.get("id"),
164
+ "preview_url": track_data.get("preview_url"),
165
+ "external_urls": track_data.get("external_urls"),
166
+ "sync_type": lyrics_data.get("syncType"),
167
+ "api_source": "syrics",
168
+ },
169
+ )
170
+
171
+ # Create segments with timing information
172
+ segments = []
173
+ for line in lyrics_data.get("lines", []):
174
+ if not line.get("words"):
175
+ continue
176
+
177
+ # Skip lines that are just musical notes
178
+ if not self._clean_lyrics(line["words"]):
179
+ continue
180
+
181
+ # Split line into words
182
+ word_texts = line["words"].strip().split()
183
+ if not word_texts:
184
+ continue
185
+
186
+ # Calculate approximate timing for each word
187
+ start_time = float(line["startTimeMs"]) / 1000 if line["startTimeMs"] != "0" else 0.0
188
+ end_time = float(line["endTimeMs"]) / 1000 if line["endTimeMs"] != "0" else 0.0
189
+ duration = end_time - start_time
190
+ word_duration = duration / len(word_texts)
191
+
192
+ words = []
193
+ for i, word_text in enumerate(word_texts):
194
+ word = Word(
195
+ id=WordUtils.generate_id(),
196
+ text=word_text,
197
+ start_time=start_time + (i * word_duration),
198
+ end_time=start_time + ((i + 1) * word_duration),
199
+ confidence=1.0,
200
+ created_during_correction=False,
201
+ )
202
+ words.append(word)
203
+
204
+ segment = LyricsSegment(
205
+ id=WordUtils.generate_id(), text=line["words"].strip(), words=words, start_time=start_time, end_time=end_time
206
+ )
207
+ segments.append(segment)
208
+
209
+ return LyricsData(source="spotify", segments=segments, metadata=metadata)
210
+
211
+ def _convert_rapidapi_format(self, raw_data: Dict[str, Any]) -> LyricsData:
212
+ """Convert RapidAPI format to standardized format."""
213
+ track_data = raw_data["track_data"]
214
+ lyrics_data = raw_data["lyrics_data"]
215
+
216
+ # Extract artist names from RapidAPI format
217
+ artist_names = []
218
+ if "artists" in track_data:
219
+ artist_names = [artist.get("name", "") for artist in track_data["artists"]]
220
+
221
+ # Create metadata object
222
+ metadata = LyricsMetadata(
223
+ source="spotify",
224
+ track_name=track_data.get("name"),
225
+ artist_names=", ".join(artist_names),
226
+ album_name=track_data.get("album", {}).get("name"),
227
+ duration_ms=track_data.get("durationMs"),
228
+ explicit=track_data.get("explicit"),
229
+ is_synced=True, # RapidAPI format includes timing information
230
+ lyrics_provider="spotify",
231
+ lyrics_provider_id=track_data.get("id"),
232
+ provider_metadata={
233
+ "spotify_id": track_data.get("id"),
234
+ "share_url": track_data.get("shareUrl"),
235
+ "duration_text": track_data.get("durationText"),
236
+ "album_cover": track_data.get("album", {}).get("cover"),
237
+ "api_source": "rapidapi",
238
+ },
239
+ )
240
+
241
+ # Create segments with timing information from RapidAPI format
242
+ segments = []
243
+ for line in lyrics_data:
244
+ if not line.get("text"):
245
+ continue
246
+
247
+ # Skip lines that are just musical notes
248
+ if not self._clean_lyrics(line["text"]):
249
+ continue
250
+
251
+ # Split line into words
252
+ word_texts = line["text"].strip().split()
253
+ if not word_texts:
254
+ continue
255
+
256
+ # Calculate timing for each word
257
+ start_time = float(line["startMs"]) / 1000 if line.get("startMs") else 0.0
258
+ duration = float(line["durMs"]) / 1000 if line.get("durMs") else 0.0
259
+ end_time = start_time + duration
260
+ word_duration = duration / len(word_texts)
261
+
262
+ words = []
263
+ for i, word_text in enumerate(word_texts):
264
+ word = Word(
265
+ id=WordUtils.generate_id(),
266
+ text=word_text,
267
+ start_time=start_time + (i * word_duration),
268
+ end_time=start_time + ((i + 1) * word_duration),
269
+ confidence=1.0,
270
+ created_during_correction=False,
271
+ )
272
+ words.append(word)
273
+
274
+ segment = LyricsSegment(
275
+ id=WordUtils.generate_id(),
276
+ text=line["text"].strip(),
277
+ words=words,
278
+ start_time=start_time,
279
+ end_time=end_time
280
+ )
281
+ segments.append(segment)
282
+
283
+ return LyricsData(source="spotify", segments=segments, metadata=metadata)
284
+
285
+ def _clean_lyrics(self, lyrics: str) -> str:
286
+ """Clean and process lyrics from Spotify to remove unwanted content."""
287
+ # Remove lines that contain only musical note symbols
288
+ if lyrics.strip() == "♪":
289
+ return ""
290
+ return lyrics
@@ -0,0 +1,44 @@
1
+ from typing import Optional, Dict, Any
2
+ from lyrics_transcriber.lyrics.base_lyrics_provider import BaseLyricsProvider, LyricsProviderConfig
3
+ from lyrics_transcriber.types import LyricsData, LyricsMetadata
4
+
5
+
6
+ class UserInputProvider(BaseLyricsProvider):
7
+ """Provider for manually input lyrics text."""
8
+
9
+ def __init__(self, lyrics_text: str, source_name: str, metadata: Dict[str, Any], *args, **kwargs):
10
+ """Initialize with the user's input text."""
11
+ super().__init__(LyricsProviderConfig(), *args, **kwargs)
12
+ self.lyrics_text = lyrics_text
13
+ self.source_name = source_name
14
+ self.input_metadata = metadata
15
+
16
+ def _fetch_data_from_source(self, artist: str, title: str) -> Optional[Dict[str, Any]]:
17
+ """Return the user's input text as raw data."""
18
+ return {"text": self.lyrics_text, "metadata": self.input_metadata}
19
+
20
+ def _convert_result_format(self, raw_data: Dict[str, Any]) -> LyricsData:
21
+ """Convert the raw text into LyricsData format."""
22
+ # Create segments with words from the text
23
+ segments = self._create_segments_with_words(raw_data["text"])
24
+
25
+ # Create metadata
26
+ metadata = LyricsMetadata(
27
+ source=self.source_name,
28
+ track_name=raw_data["metadata"].get("title", ""),
29
+ artist_names=raw_data["metadata"].get("artist", ""),
30
+ is_synced=False,
31
+ lyrics_provider="manual",
32
+ lyrics_provider_id="",
33
+ album_name=None,
34
+ duration_ms=None,
35
+ explicit=None,
36
+ language=None,
37
+ provider_metadata={},
38
+ )
39
+
40
+ return LyricsData(segments=segments, metadata=metadata, source=self.source_name)
41
+
42
+ def get_name(self) -> str:
43
+ """Return the provider name."""
44
+ return "UserInput"
File without changes
@@ -0,0 +1,21 @@
1
+ from lyrics_transcriber.output.ass.lyrics_screen import LyricsScreen
2
+ from lyrics_transcriber.output.ass.lyrics_line import LyricsLine
3
+ from lyrics_transcriber.output.ass.section_screen import SectionScreen
4
+ from lyrics_transcriber.output.ass.style import Style
5
+ from lyrics_transcriber.output.ass.event import Event
6
+ from lyrics_transcriber.output.ass.config import (
7
+ ScreenConfig,
8
+ LineTimingInfo,
9
+ LineState,
10
+ )
11
+
12
+ __all__ = [
13
+ 'LyricsScreen',
14
+ 'LyricsLine',
15
+ 'SectionScreen',
16
+ 'Style',
17
+ 'Event',
18
+ 'ScreenConfig',
19
+ 'LineTimingInfo',
20
+ 'LineState',
21
+ ]