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.

Files changed (287) hide show
  1. karaoke_gen/__init__.py +38 -0
  2. karaoke_gen/audio_fetcher.py +1614 -0
  3. karaoke_gen/audio_processor.py +790 -0
  4. karaoke_gen/config.py +83 -0
  5. karaoke_gen/file_handler.py +387 -0
  6. karaoke_gen/instrumental_review/__init__.py +45 -0
  7. karaoke_gen/instrumental_review/analyzer.py +408 -0
  8. karaoke_gen/instrumental_review/editor.py +322 -0
  9. karaoke_gen/instrumental_review/models.py +171 -0
  10. karaoke_gen/instrumental_review/server.py +475 -0
  11. karaoke_gen/instrumental_review/static/index.html +1529 -0
  12. karaoke_gen/instrumental_review/waveform.py +409 -0
  13. karaoke_gen/karaoke_finalise/__init__.py +1 -0
  14. karaoke_gen/karaoke_finalise/karaoke_finalise.py +1833 -0
  15. karaoke_gen/karaoke_gen.py +1026 -0
  16. karaoke_gen/lyrics_processor.py +474 -0
  17. karaoke_gen/metadata.py +160 -0
  18. karaoke_gen/pipeline/__init__.py +87 -0
  19. karaoke_gen/pipeline/base.py +215 -0
  20. karaoke_gen/pipeline/context.py +230 -0
  21. karaoke_gen/pipeline/executors/__init__.py +21 -0
  22. karaoke_gen/pipeline/executors/local.py +159 -0
  23. karaoke_gen/pipeline/executors/remote.py +257 -0
  24. karaoke_gen/pipeline/stages/__init__.py +27 -0
  25. karaoke_gen/pipeline/stages/finalize.py +202 -0
  26. karaoke_gen/pipeline/stages/render.py +165 -0
  27. karaoke_gen/pipeline/stages/screens.py +139 -0
  28. karaoke_gen/pipeline/stages/separation.py +191 -0
  29. karaoke_gen/pipeline/stages/transcription.py +191 -0
  30. karaoke_gen/resources/AvenirNext-Bold.ttf +0 -0
  31. karaoke_gen/resources/Montserrat-Bold.ttf +0 -0
  32. karaoke_gen/resources/Oswald-Bold.ttf +0 -0
  33. karaoke_gen/resources/Oswald-SemiBold.ttf +0 -0
  34. karaoke_gen/resources/Zurich_Cn_BT_Bold.ttf +0 -0
  35. karaoke_gen/style_loader.py +531 -0
  36. karaoke_gen/utils/__init__.py +18 -0
  37. karaoke_gen/utils/bulk_cli.py +492 -0
  38. karaoke_gen/utils/cli_args.py +432 -0
  39. karaoke_gen/utils/gen_cli.py +978 -0
  40. karaoke_gen/utils/remote_cli.py +3268 -0
  41. karaoke_gen/video_background_processor.py +351 -0
  42. karaoke_gen/video_generator.py +424 -0
  43. karaoke_gen-0.75.54.dist-info/METADATA +718 -0
  44. karaoke_gen-0.75.54.dist-info/RECORD +287 -0
  45. karaoke_gen-0.75.54.dist-info/WHEEL +4 -0
  46. karaoke_gen-0.75.54.dist-info/entry_points.txt +5 -0
  47. karaoke_gen-0.75.54.dist-info/licenses/LICENSE +21 -0
  48. lyrics_transcriber/__init__.py +10 -0
  49. lyrics_transcriber/cli/__init__.py +0 -0
  50. lyrics_transcriber/cli/cli_main.py +285 -0
  51. lyrics_transcriber/core/__init__.py +0 -0
  52. lyrics_transcriber/core/config.py +50 -0
  53. lyrics_transcriber/core/controller.py +594 -0
  54. lyrics_transcriber/correction/__init__.py +0 -0
  55. lyrics_transcriber/correction/agentic/__init__.py +9 -0
  56. lyrics_transcriber/correction/agentic/adapter.py +71 -0
  57. lyrics_transcriber/correction/agentic/agent.py +313 -0
  58. lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
  59. lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
  60. lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
  61. lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
  62. lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
  63. lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
  64. lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
  65. lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
  66. lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
  67. lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
  68. lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
  69. lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
  70. lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
  71. lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
  72. lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
  73. lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
  74. lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
  75. lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
  76. lyrics_transcriber/correction/agentic/models/enums.py +38 -0
  77. lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
  78. lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
  79. lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
  80. lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
  81. lyrics_transcriber/correction/agentic/models/utils.py +19 -0
  82. lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
  83. lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
  84. lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
  85. lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
  86. lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
  87. lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
  88. lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
  89. lyrics_transcriber/correction/agentic/providers/base.py +36 -0
  90. lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
  91. lyrics_transcriber/correction/agentic/providers/config.py +73 -0
  92. lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
  93. lyrics_transcriber/correction/agentic/providers/health.py +28 -0
  94. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
  95. lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
  96. lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
  97. lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
  98. lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
  99. lyrics_transcriber/correction/agentic/router.py +35 -0
  100. lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
  101. lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
  102. lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
  103. lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
  104. lyrics_transcriber/correction/anchor_sequence.py +919 -0
  105. lyrics_transcriber/correction/corrector.py +760 -0
  106. lyrics_transcriber/correction/feedback/__init__.py +2 -0
  107. lyrics_transcriber/correction/feedback/schemas.py +107 -0
  108. lyrics_transcriber/correction/feedback/store.py +236 -0
  109. lyrics_transcriber/correction/handlers/__init__.py +0 -0
  110. lyrics_transcriber/correction/handlers/base.py +52 -0
  111. lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
  112. lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
  113. lyrics_transcriber/correction/handlers/llm.py +293 -0
  114. lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
  115. lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
  116. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
  117. lyrics_transcriber/correction/handlers/repeat.py +88 -0
  118. lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
  119. lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
  120. lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
  121. lyrics_transcriber/correction/handlers/word_operations.py +187 -0
  122. lyrics_transcriber/correction/operations.py +352 -0
  123. lyrics_transcriber/correction/phrase_analyzer.py +435 -0
  124. lyrics_transcriber/correction/text_utils.py +30 -0
  125. lyrics_transcriber/frontend/.gitignore +23 -0
  126. lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
  127. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  128. lyrics_transcriber/frontend/README.md +50 -0
  129. lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
  130. lyrics_transcriber/frontend/__init__.py +25 -0
  131. lyrics_transcriber/frontend/eslint.config.js +28 -0
  132. lyrics_transcriber/frontend/index.html +18 -0
  133. lyrics_transcriber/frontend/package.json +42 -0
  134. lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
  135. lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
  136. lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
  137. lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
  138. lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
  139. lyrics_transcriber/frontend/public/favicon.ico +0 -0
  140. lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
  141. lyrics_transcriber/frontend/src/App.tsx +214 -0
  142. lyrics_transcriber/frontend/src/api.ts +254 -0
  143. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
  144. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
  145. lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
  146. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
  147. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
  148. lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
  149. lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
  150. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
  151. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
  152. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
  153. lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
  154. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
  155. lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
  156. lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
  157. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
  158. lyrics_transcriber/frontend/src/components/Header.tsx +413 -0
  159. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1387 -0
  160. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +185 -0
  161. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +704 -0
  162. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +80 -0
  163. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +905 -0
  164. lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
  165. lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +127 -0
  166. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
  167. lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
  168. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
  169. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
  170. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +336 -0
  171. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
  172. lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
  173. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
  174. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
  175. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
  176. lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
  177. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
  178. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
  179. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
  180. lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
  181. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
  182. lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
  183. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  184. lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
  185. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
  186. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  187. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
  188. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
  189. lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
  190. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  191. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
  192. lyrics_transcriber/frontend/src/main.tsx +17 -0
  193. lyrics_transcriber/frontend/src/theme.ts +177 -0
  194. lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
  195. lyrics_transcriber/frontend/src/types.js +2 -0
  196. lyrics_transcriber/frontend/src/types.ts +199 -0
  197. lyrics_transcriber/frontend/src/validation.ts +132 -0
  198. lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
  199. lyrics_transcriber/frontend/tsconfig.app.json +26 -0
  200. lyrics_transcriber/frontend/tsconfig.json +25 -0
  201. lyrics_transcriber/frontend/tsconfig.node.json +23 -0
  202. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
  203. lyrics_transcriber/frontend/update_version.js +11 -0
  204. lyrics_transcriber/frontend/vite.config.d.ts +2 -0
  205. lyrics_transcriber/frontend/vite.config.js +10 -0
  206. lyrics_transcriber/frontend/vite.config.ts +11 -0
  207. lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
  208. lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
  209. lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
  210. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js +43288 -0
  211. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +1 -0
  212. lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
  213. lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
  214. lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
  215. lyrics_transcriber/frontend/web_assets/index.html +18 -0
  216. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
  217. lyrics_transcriber/frontend/yarn.lock +3752 -0
  218. lyrics_transcriber/lyrics/__init__.py +0 -0
  219. lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
  220. lyrics_transcriber/lyrics/file_provider.py +95 -0
  221. lyrics_transcriber/lyrics/genius.py +384 -0
  222. lyrics_transcriber/lyrics/lrclib.py +231 -0
  223. lyrics_transcriber/lyrics/musixmatch.py +156 -0
  224. lyrics_transcriber/lyrics/spotify.py +290 -0
  225. lyrics_transcriber/lyrics/user_input_provider.py +44 -0
  226. lyrics_transcriber/output/__init__.py +0 -0
  227. lyrics_transcriber/output/ass/__init__.py +21 -0
  228. lyrics_transcriber/output/ass/ass.py +2088 -0
  229. lyrics_transcriber/output/ass/ass_specs.txt +732 -0
  230. lyrics_transcriber/output/ass/config.py +180 -0
  231. lyrics_transcriber/output/ass/constants.py +23 -0
  232. lyrics_transcriber/output/ass/event.py +94 -0
  233. lyrics_transcriber/output/ass/formatters.py +132 -0
  234. lyrics_transcriber/output/ass/lyrics_line.py +265 -0
  235. lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
  236. lyrics_transcriber/output/ass/section_detector.py +89 -0
  237. lyrics_transcriber/output/ass/section_screen.py +106 -0
  238. lyrics_transcriber/output/ass/style.py +187 -0
  239. lyrics_transcriber/output/cdg.py +619 -0
  240. lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
  241. lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
  242. lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
  243. lyrics_transcriber/output/cdgmaker/config.py +151 -0
  244. lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
  245. lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
  246. lyrics_transcriber/output/cdgmaker/pack.py +507 -0
  247. lyrics_transcriber/output/cdgmaker/render.py +346 -0
  248. lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
  249. lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
  250. lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
  251. lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
  252. lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
  253. lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
  254. lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
  255. lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
  256. lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
  257. lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
  258. lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
  259. lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
  260. lyrics_transcriber/output/cdgmaker/utils.py +132 -0
  261. lyrics_transcriber/output/countdown_processor.py +306 -0
  262. lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
  263. lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
  264. lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
  265. lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
  266. lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
  267. lyrics_transcriber/output/fonts/arial.ttf +0 -0
  268. lyrics_transcriber/output/fonts/georgia.ttf +0 -0
  269. lyrics_transcriber/output/fonts/verdana.ttf +0 -0
  270. lyrics_transcriber/output/generator.py +257 -0
  271. lyrics_transcriber/output/lrc_to_cdg.py +61 -0
  272. lyrics_transcriber/output/lyrics_file.py +102 -0
  273. lyrics_transcriber/output/plain_text.py +96 -0
  274. lyrics_transcriber/output/segment_resizer.py +431 -0
  275. lyrics_transcriber/output/subtitles.py +397 -0
  276. lyrics_transcriber/output/video.py +544 -0
  277. lyrics_transcriber/review/__init__.py +0 -0
  278. lyrics_transcriber/review/server.py +676 -0
  279. lyrics_transcriber/storage/__init__.py +0 -0
  280. lyrics_transcriber/storage/dropbox.py +225 -0
  281. lyrics_transcriber/transcribers/__init__.py +0 -0
  282. lyrics_transcriber/transcribers/audioshake.py +379 -0
  283. lyrics_transcriber/transcribers/base_transcriber.py +157 -0
  284. lyrics_transcriber/transcribers/whisper.py +330 -0
  285. lyrics_transcriber/types.py +650 -0
  286. lyrics_transcriber/utils/__init__.py +0 -0
  287. lyrics_transcriber/utils/word_utils.py +27 -0
@@ -0,0 +1,474 @@
1
+ import os
2
+ import re
3
+ import logging
4
+ import shutil
5
+ import json
6
+ from lyrics_transcriber import LyricsTranscriber, OutputConfig, TranscriberConfig, LyricsConfig
7
+ from lyrics_transcriber.core.controller import LyricsControllerResult
8
+ from dotenv import load_dotenv
9
+ from .utils import sanitize_filename
10
+
11
+
12
+ # Placeholder class or functions for lyrics processing
13
+ class LyricsProcessor:
14
+ # Standard countdown padding duration used by LyricsTranscriber
15
+ COUNTDOWN_PADDING_SECONDS = 3.0
16
+
17
+ def __init__(
18
+ self, logger, style_params_json, lyrics_file, skip_transcription, skip_transcription_review, render_video, subtitle_offset_ms
19
+ ):
20
+ self.logger = logger
21
+ self.style_params_json = style_params_json
22
+ self.lyrics_file = lyrics_file
23
+ self.skip_transcription = skip_transcription
24
+ self.skip_transcription_review = skip_transcription_review
25
+ self.render_video = render_video
26
+ self.subtitle_offset_ms = subtitle_offset_ms
27
+
28
+ def _detect_countdown_padding_from_lrc(self, lrc_filepath):
29
+ """
30
+ Detect if countdown padding was applied by checking for countdown text in the LRC file.
31
+
32
+ The countdown segment has the text "3... 2... 1..." at timestamp 0.1-2.9s.
33
+ We detect this by looking for the countdown text pattern.
34
+
35
+ Args:
36
+ lrc_filepath: Path to the LRC file
37
+
38
+ Returns:
39
+ Tuple of (countdown_padding_added: bool, countdown_padding_seconds: float)
40
+ """
41
+ try:
42
+ with open(lrc_filepath, 'r', encoding='utf-8') as f:
43
+ content = f.read()
44
+
45
+ # Method 1: Check for countdown text pattern "3... 2... 1..."
46
+ # This is the most reliable detection method since the countdown text is unique
47
+ countdown_text = "3... 2... 1..."
48
+ if countdown_text in content:
49
+ self.logger.info(f"Detected countdown padding from LRC: found countdown text '{countdown_text}'")
50
+ return (True, self.COUNTDOWN_PADDING_SECONDS)
51
+
52
+ # Method 2 (fallback): Check if first lyric timestamp is >= 3 seconds
53
+ # This handles cases where countdown text format might differ
54
+ # LRC timestamps: [mm:ss.xx] or [mm:ss.xxx]
55
+ timestamp_pattern = r'\[(\d{1,2}):(\d{2})\.(\d{2,3})\]'
56
+ matches = re.findall(timestamp_pattern, content)
57
+
58
+ if not matches:
59
+ self.logger.debug("No timestamps found in LRC file")
60
+ return (False, 0.0)
61
+
62
+ # Parse the first timestamp
63
+ first_timestamp = matches[0]
64
+ minutes = int(first_timestamp[0])
65
+ seconds = int(first_timestamp[1])
66
+ # Handle both .xx and .xxx formats
67
+ centiseconds = first_timestamp[2]
68
+ if len(centiseconds) == 2:
69
+ milliseconds = int(centiseconds) * 10
70
+ else:
71
+ milliseconds = int(centiseconds)
72
+
73
+ first_lyric_time = minutes * 60 + seconds + milliseconds / 1000.0
74
+
75
+ self.logger.debug(f"First lyric timestamp in LRC: {first_lyric_time:.3f}s")
76
+
77
+ # If first lyric is at or after 3 seconds, countdown padding was applied
78
+ # Use a small buffer (2.5s) to account for songs that naturally start a bit late
79
+ if first_lyric_time >= 2.5:
80
+ self.logger.info(f"Detected countdown padding from LRC: first lyric at {first_lyric_time:.2f}s")
81
+ return (True, self.COUNTDOWN_PADDING_SECONDS)
82
+
83
+ return (False, 0.0)
84
+
85
+ except Exception as e:
86
+ self.logger.warning(f"Failed to detect countdown padding from LRC file: {e}")
87
+ return (False, 0.0)
88
+
89
+ def find_best_split_point(self, line):
90
+ """
91
+ Find the best split point in a line based on the specified criteria.
92
+ """
93
+
94
+ self.logger.debug(f"Finding best_split_point for line: {line}")
95
+ words = line.split()
96
+ mid_word_index = len(words) // 2
97
+ self.logger.debug(f"words: {words} mid_word_index: {mid_word_index}")
98
+
99
+ # Check for a comma within one or two words of the middle word
100
+ if "," in line:
101
+ mid_point = len(" ".join(words[:mid_word_index]))
102
+ comma_indices = [i for i, char in enumerate(line) if char == ","]
103
+
104
+ for index in comma_indices:
105
+ if abs(mid_point - index) < 20 and len(line[: index + 1].strip()) <= 36:
106
+ self.logger.debug(
107
+ f"Found comma at index {index} which is within 20 characters of mid_point {mid_point} and results in a suitable line length, accepting as split point"
108
+ )
109
+ return index + 1 # Include the comma in the first line
110
+
111
+ # Check for 'and'
112
+ if " and " in line:
113
+ mid_point = len(line) // 2
114
+ and_indices = [m.start() for m in re.finditer(" and ", line)]
115
+ for index in sorted(and_indices, key=lambda x: abs(x - mid_point)):
116
+ if len(line[: index + len(" and ")].strip()) <= 36:
117
+ self.logger.debug(f"Found 'and' at index {index} which results in a suitable line length, accepting as split point")
118
+ return index + len(" and ")
119
+
120
+ # If no better split point is found, try splitting at the middle word
121
+ if len(words) > 2 and mid_word_index > 0:
122
+ split_at_middle = len(" ".join(words[:mid_word_index]))
123
+ if split_at_middle <= 36:
124
+ self.logger.debug(f"Splitting at middle word index: {mid_word_index}")
125
+ return split_at_middle
126
+
127
+ # If the line is still too long, forcibly split at the maximum length
128
+ forced_split_point = 36
129
+ if len(line) > forced_split_point:
130
+ self.logger.debug(f"Line is still too long, forcibly splitting at position {forced_split_point}")
131
+ return forced_split_point
132
+
133
+ def process_line(self, line):
134
+ """
135
+ Process a single line to ensure it's within the maximum length,
136
+ and handle parentheses.
137
+ """
138
+ processed_lines = []
139
+ iteration_count = 0
140
+ max_iterations = 100 # Failsafe limit
141
+
142
+ while len(line) > 36:
143
+ if iteration_count > max_iterations:
144
+ self.logger.error(f"Maximum iterations exceeded in process_line for line: {line}")
145
+ break
146
+
147
+ # Check if the line contains parentheses
148
+ if "(" in line and ")" in line:
149
+ start_paren = line.find("(")
150
+ end_paren = line.find(")") + 1
151
+ if end_paren < len(line) and line[end_paren] == ",":
152
+ end_paren += 1
153
+
154
+ if start_paren > 0:
155
+ processed_lines.append(line[:start_paren].strip())
156
+ processed_lines.append(line[start_paren:end_paren].strip())
157
+ line = line[end_paren:].strip()
158
+ else:
159
+ split_point = self.find_best_split_point(line)
160
+ processed_lines.append(line[:split_point].strip())
161
+ line = line[split_point:].strip()
162
+
163
+ iteration_count += 1
164
+
165
+ if line: # Add the remaining part if not empty
166
+ processed_lines.append(line)
167
+
168
+ return processed_lines
169
+
170
+ def _check_transcription_providers(self) -> dict:
171
+ """
172
+ Check which transcription providers are configured and return their status.
173
+
174
+ Returns:
175
+ dict with 'configured' (list of provider names) and 'missing' (list of missing configs)
176
+ """
177
+ load_dotenv()
178
+
179
+ configured = []
180
+ missing = []
181
+
182
+ # Check AudioShake
183
+ audioshake_token = os.getenv("AUDIOSHAKE_API_TOKEN")
184
+ if audioshake_token:
185
+ configured.append("AudioShake")
186
+ self.logger.debug("AudioShake transcription provider: configured")
187
+ else:
188
+ missing.append("AudioShake (AUDIOSHAKE_API_TOKEN)")
189
+ self.logger.debug("AudioShake transcription provider: not configured (missing AUDIOSHAKE_API_TOKEN)")
190
+
191
+ # Check Whisper via RunPod
192
+ runpod_key = os.getenv("RUNPOD_API_KEY")
193
+ whisper_id = os.getenv("WHISPER_RUNPOD_ID")
194
+ if runpod_key and whisper_id:
195
+ configured.append("Whisper (RunPod)")
196
+ self.logger.debug("Whisper transcription provider: configured")
197
+ elif runpod_key:
198
+ missing.append("Whisper (missing WHISPER_RUNPOD_ID)")
199
+ self.logger.debug("Whisper transcription provider: partially configured (missing WHISPER_RUNPOD_ID)")
200
+ elif whisper_id:
201
+ missing.append("Whisper (missing RUNPOD_API_KEY)")
202
+ self.logger.debug("Whisper transcription provider: partially configured (missing RUNPOD_API_KEY)")
203
+ else:
204
+ missing.append("Whisper (RUNPOD_API_KEY + WHISPER_RUNPOD_ID)")
205
+ self.logger.debug("Whisper transcription provider: not configured")
206
+
207
+ return {"configured": configured, "missing": missing}
208
+
209
+ def _build_transcription_provider_error_message(self, missing_providers: list) -> str:
210
+ """Build a helpful error message when no transcription providers are configured."""
211
+ return (
212
+ "No transcription providers configured!\n"
213
+ "\n"
214
+ "Karaoke video generation requires at least one transcription provider to create "
215
+ "synchronized lyrics. Without a transcription provider, the system cannot generate "
216
+ "the word-level timing data needed for the karaoke video.\n"
217
+ "\n"
218
+ "AVAILABLE TRANSCRIPTION PROVIDERS:\n"
219
+ "\n"
220
+ "1. AudioShake (Recommended - Commercial, high-quality)\n"
221
+ " - Set environment variable: AUDIOSHAKE_API_TOKEN=your_token\n"
222
+ " - Get an API key at: https://www.audioshake.ai/\n"
223
+ "\n"
224
+ "2. Whisper via RunPod (Open-source alternative)\n"
225
+ " - Set environment variables:\n"
226
+ " RUNPOD_API_KEY=your_key\n"
227
+ " WHISPER_RUNPOD_ID=your_endpoint_id\n"
228
+ " - Set up a Whisper endpoint at: https://www.runpod.io/\n"
229
+ "\n"
230
+ "ALTERNATIVES:\n"
231
+ "\n"
232
+ "- Use --skip-lyrics flag to generate instrumental-only karaoke (no synchronized lyrics)\n"
233
+ "- Use --lyrics_file to provide pre-timed lyrics (still needs transcription for timing)\n"
234
+ "\n"
235
+ f"Missing provider configurations: {', '.join(missing_providers)}\n"
236
+ "\n"
237
+ "See README.md 'Transcription Providers' section for detailed setup instructions."
238
+ )
239
+
240
+ def transcribe_lyrics(self, input_audio_wav, artist, title, track_output_dir, lyrics_artist=None, lyrics_title=None):
241
+ """
242
+ Transcribe lyrics for a track.
243
+
244
+ Args:
245
+ input_audio_wav: Path to the audio file
246
+ artist: Original artist name (used for filename generation)
247
+ title: Original title (used for filename generation)
248
+ track_output_dir: Output directory path
249
+ lyrics_artist: Artist name for lyrics processing (defaults to artist if None)
250
+ lyrics_title: Title for lyrics processing (defaults to title if None)
251
+
252
+ Raises:
253
+ ValueError: If transcription is enabled but no providers are configured
254
+ """
255
+ # Use original artist/title for filename generation
256
+ filename_artist = artist
257
+ filename_title = title
258
+
259
+ # Use lyrics_artist/lyrics_title for actual lyrics processing, fall back to originals if not provided
260
+ processing_artist = lyrics_artist or artist
261
+ processing_title = lyrics_title or title
262
+
263
+ self.logger.info(
264
+ f"Transcribing lyrics for track {processing_artist} - {processing_title} from audio file: {input_audio_wav} with output directory: {track_output_dir}"
265
+ )
266
+
267
+ # Check for existing files first using sanitized names from ORIGINAL artist/title for consistency
268
+ sanitized_artist = sanitize_filename(filename_artist)
269
+ sanitized_title = sanitize_filename(filename_title)
270
+ parent_video_path = os.path.join(track_output_dir, f"{sanitized_artist} - {sanitized_title} (With Vocals).mkv")
271
+ parent_lrc_path = os.path.join(track_output_dir, f"{sanitized_artist} - {sanitized_title} (Karaoke).lrc")
272
+
273
+ # Check lyrics directory for existing files
274
+ lyrics_dir = os.path.join(track_output_dir, "lyrics")
275
+ lyrics_video_path = os.path.join(lyrics_dir, f"{sanitized_artist} - {sanitized_title} (With Vocals).mkv")
276
+ lyrics_lrc_path = os.path.join(lyrics_dir, f"{sanitized_artist} - {sanitized_title} (Karaoke).lrc")
277
+
278
+ # If files exist in parent directory, return early (but detect countdown padding first)
279
+ if os.path.exists(parent_video_path) and os.path.exists(parent_lrc_path):
280
+ self.logger.info("Found existing video and LRC files in parent directory, skipping transcription")
281
+
282
+ # Detect countdown padding from existing LRC file
283
+ countdown_padding_added, countdown_padding_seconds = self._detect_countdown_padding_from_lrc(parent_lrc_path)
284
+
285
+ if countdown_padding_added:
286
+ self.logger.info(f"Existing files have countdown padding: {countdown_padding_seconds}s")
287
+
288
+ return {
289
+ "lrc_filepath": parent_lrc_path,
290
+ "ass_filepath": parent_video_path,
291
+ "countdown_padding_added": countdown_padding_added,
292
+ "countdown_padding_seconds": countdown_padding_seconds,
293
+ "padded_audio_filepath": None, # Original padded audio may not exist
294
+ }
295
+
296
+ # If files exist in lyrics directory, copy to parent and return (but detect countdown padding first)
297
+ if os.path.exists(lyrics_video_path) and os.path.exists(lyrics_lrc_path):
298
+ self.logger.info("Found existing video and LRC files in lyrics directory, copying to parent")
299
+ os.makedirs(track_output_dir, exist_ok=True)
300
+ shutil.copy2(lyrics_video_path, parent_video_path)
301
+ shutil.copy2(lyrics_lrc_path, parent_lrc_path)
302
+
303
+ # Detect countdown padding from existing LRC file
304
+ countdown_padding_added, countdown_padding_seconds = self._detect_countdown_padding_from_lrc(parent_lrc_path)
305
+
306
+ if countdown_padding_added:
307
+ self.logger.info(f"Existing files have countdown padding: {countdown_padding_seconds}s")
308
+
309
+ return {
310
+ "lrc_filepath": parent_lrc_path,
311
+ "ass_filepath": parent_video_path,
312
+ "countdown_padding_added": countdown_padding_added,
313
+ "countdown_padding_seconds": countdown_padding_seconds,
314
+ "padded_audio_filepath": None, # Original padded audio may not exist
315
+ }
316
+
317
+ # Check transcription provider configuration if transcription is not being skipped
318
+ # Do this AFTER checking for existing files, since existing files don't need transcription
319
+ if not self.skip_transcription:
320
+ provider_status = self._check_transcription_providers()
321
+
322
+ if provider_status["configured"]:
323
+ self.logger.info(f"Transcription providers configured: {', '.join(provider_status['configured'])}")
324
+ else:
325
+ error_msg = self._build_transcription_provider_error_message(provider_status["missing"])
326
+ raise ValueError(error_msg)
327
+
328
+ # Create lyrics directory if it doesn't exist
329
+ os.makedirs(lyrics_dir, exist_ok=True)
330
+ self.logger.info(f"Created lyrics directory: {lyrics_dir}")
331
+
332
+ # Set render_video to False if explicitly disabled
333
+ render_video = self.render_video
334
+ if not render_video:
335
+ self.logger.info("Video rendering disabled, skipping video output")
336
+
337
+ # Load environment variables
338
+ load_dotenv()
339
+ env_config = {
340
+ "audioshake_api_token": os.getenv("AUDIOSHAKE_API_TOKEN"),
341
+ "genius_api_token": os.getenv("GENIUS_API_TOKEN"),
342
+ "spotify_cookie": os.getenv("SPOTIFY_COOKIE_SP_DC"),
343
+ "runpod_api_key": os.getenv("RUNPOD_API_KEY"),
344
+ "whisper_runpod_id": os.getenv("WHISPER_RUNPOD_ID"),
345
+ "rapidapi_key": os.getenv("RAPIDAPI_KEY"), # Add missing RAPIDAPI_KEY
346
+ }
347
+
348
+ # Create config objects for LyricsTranscriber
349
+ transcriber_config = TranscriberConfig(
350
+ audioshake_api_token=env_config.get("audioshake_api_token"),
351
+ )
352
+
353
+ lyrics_config = LyricsConfig(
354
+ genius_api_token=env_config.get("genius_api_token"),
355
+ spotify_cookie=env_config.get("spotify_cookie"),
356
+ rapidapi_key=env_config.get("rapidapi_key"),
357
+ lyrics_file=self.lyrics_file,
358
+ )
359
+
360
+ # Debug logging for lyrics_config
361
+ self.logger.info(f"LyricsConfig created with:")
362
+ self.logger.info(f" genius_api_token: {env_config.get('genius_api_token')[:3] + '...' if env_config.get('genius_api_token') else 'None'}")
363
+ self.logger.info(f" spotify_cookie: {env_config.get('spotify_cookie')[:3] + '...' if env_config.get('spotify_cookie') else 'None'}")
364
+ self.logger.info(f" rapidapi_key: {env_config.get('rapidapi_key')[:3] + '...' if env_config.get('rapidapi_key') else 'None'}")
365
+ self.logger.info(f" lyrics_file: {self.lyrics_file}")
366
+
367
+ # Detect if we're running in a serverless environment (Modal)
368
+ # Modal sets specific environment variables we can check for
369
+ is_serverless = (
370
+ os.getenv("MODAL_TASK_ID") is not None or
371
+ os.getenv("MODAL_FUNCTION_NAME") is not None or
372
+ os.path.exists("/.modal") # Modal creates this directory in containers
373
+ )
374
+
375
+ # In serverless environment, disable interactive review even if skip_transcription_review=False
376
+ # This preserves CLI behavior while fixing serverless hanging
377
+ enable_review_setting = not self.skip_transcription_review and not is_serverless
378
+
379
+ if is_serverless and not self.skip_transcription_review:
380
+ self.logger.info("Detected serverless environment - disabling interactive review to prevent hanging")
381
+
382
+ # In serverless environment, disable video generation during Phase 1 to save compute
383
+ # Video will be generated in Phase 2 after human review
384
+ serverless_render_video = render_video and not is_serverless
385
+
386
+ if is_serverless and render_video:
387
+ self.logger.info("Detected serverless environment - deferring video generation until after review")
388
+
389
+ output_config = OutputConfig(
390
+ output_styles_json=self.style_params_json,
391
+ output_dir=lyrics_dir,
392
+ render_video=serverless_render_video, # Disable video in serverless Phase 1
393
+ fetch_lyrics=True,
394
+ run_transcription=not self.skip_transcription,
395
+ run_correction=True,
396
+ generate_plain_text=True,
397
+ generate_lrc=True,
398
+ generate_cdg=False, # Also defer CDG generation to Phase 2
399
+ video_resolution="4k",
400
+ enable_review=enable_review_setting,
401
+ subtitle_offset_ms=self.subtitle_offset_ms,
402
+ )
403
+
404
+ # Add this log entry to debug the OutputConfig
405
+ self.logger.info(f"Instantiating LyricsTranscriber with OutputConfig: {output_config}")
406
+
407
+ # Initialize transcriber with new config objects - use PROCESSING artist/title for lyrics work
408
+ transcriber = LyricsTranscriber(
409
+ audio_filepath=input_audio_wav,
410
+ artist=processing_artist, # Use lyrics_artist for processing
411
+ title=processing_title, # Use lyrics_title for processing
412
+ transcriber_config=transcriber_config,
413
+ lyrics_config=lyrics_config,
414
+ output_config=output_config,
415
+ logger=self.logger,
416
+ )
417
+
418
+ # Process and get results
419
+ results: LyricsControllerResult = transcriber.process()
420
+ self.logger.info(f"Transcriber Results Filepaths:")
421
+ for key, value in results.__dict__.items():
422
+ if key.endswith("_filepath"):
423
+ self.logger.info(f" {key}: {value}")
424
+
425
+ # Build output dictionary
426
+ transcriber_outputs = {}
427
+ if results.lrc_filepath:
428
+ transcriber_outputs["lrc_filepath"] = results.lrc_filepath
429
+ self.logger.info(f"Moving LRC file from {results.lrc_filepath} to {parent_lrc_path}")
430
+ shutil.copy2(results.lrc_filepath, parent_lrc_path)
431
+
432
+ if results.ass_filepath:
433
+ transcriber_outputs["ass_filepath"] = results.ass_filepath
434
+ self.logger.info(f"Moving video file from {results.video_filepath} to {parent_video_path}")
435
+ shutil.copy2(results.video_filepath, parent_video_path)
436
+
437
+ if results.transcription_corrected:
438
+ transcriber_outputs["corrected_lyrics_text"] = "\n".join(
439
+ segment.text for segment in results.transcription_corrected.corrected_segments
440
+ )
441
+ transcriber_outputs["corrected_lyrics_text_filepath"] = results.corrected_txt
442
+
443
+ # Save correction data to JSON file for review interface
444
+ # Use the expected filename format: "{artist} - {title} (Lyrics Corrections).json"
445
+ # Use sanitized names to be consistent with all other files created by lyrics_transcriber
446
+ corrections_filename = f"{sanitized_artist} - {sanitized_title} (Lyrics Corrections).json"
447
+ corrections_filepath = os.path.join(lyrics_dir, corrections_filename)
448
+
449
+ # Use the CorrectionResult's to_dict() method to serialize
450
+ correction_data = results.transcription_corrected.to_dict()
451
+
452
+ with open(corrections_filepath, 'w') as f:
453
+ json.dump(correction_data, f, indent=2)
454
+
455
+ self.logger.info(f"Saved correction data to {corrections_filepath}")
456
+
457
+ # Capture countdown padding information for syncing with instrumental audio
458
+ transcriber_outputs["countdown_padding_added"] = getattr(results, "countdown_padding_added", False)
459
+ transcriber_outputs["countdown_padding_seconds"] = getattr(results, "countdown_padding_seconds", 0.0)
460
+ transcriber_outputs["padded_audio_filepath"] = getattr(results, "padded_audio_filepath", None)
461
+
462
+ if transcriber_outputs["countdown_padding_added"]:
463
+ self.logger.info(
464
+ f"Countdown padding detected: {transcriber_outputs['countdown_padding_seconds']}s added to vocals. "
465
+ f"Instrumental audio will need to be padded accordingly."
466
+ )
467
+
468
+ if transcriber_outputs:
469
+ self.logger.info(f"*** Transcriber Filepath Outputs: ***")
470
+ for key, value in transcriber_outputs.items():
471
+ if key.endswith("_filepath"):
472
+ self.logger.info(f" {key}: {value}")
473
+
474
+ return transcriber_outputs
@@ -0,0 +1,160 @@
1
+ import logging
2
+
3
+
4
+ def extract_info_for_online_media(input_url, input_artist, input_title, logger, cookies_str=None):
5
+ """
6
+ Creates metadata info dict from provided artist and title.
7
+
8
+ Note: This function no longer supports URL-based metadata extraction.
9
+ Audio search and download is now handled by the AudioFetcher class using flacfetch.
10
+
11
+ When both artist and title are provided, this creates a metadata dict that can be
12
+ used by the rest of the pipeline.
13
+
14
+ Args:
15
+ input_url: Deprecated - URLs should be provided as local file paths or use AudioFetcher
16
+ input_artist: The artist name
17
+ input_title: The track title
18
+ logger: Logger instance
19
+ cookies_str: Deprecated - no longer used
20
+
21
+ Returns:
22
+ A dict with metadata if artist and title are provided
23
+
24
+ Raises:
25
+ ValueError: If URL is provided (deprecated) or if artist/title are missing
26
+ """
27
+ logger.info(f"Extracting info for input_url: {input_url} input_artist: {input_artist} input_title: {input_title}")
28
+
29
+ # URLs are no longer supported - use AudioFetcher for search and download
30
+ if input_url is not None:
31
+ raise ValueError(
32
+ "URL-based audio fetching has been replaced with flacfetch. "
33
+ "Please provide a local file path instead, or use artist and title only "
34
+ "to search for audio via flacfetch."
35
+ )
36
+
37
+ # When artist and title are provided, create a synthetic metadata dict
38
+ # The actual search and download is handled by AudioFetcher
39
+ if input_artist and input_title:
40
+ logger.info(f"Creating metadata for: {input_artist} - {input_title}")
41
+ return {
42
+ "title": f"{input_artist} - {input_title}",
43
+ "artist": input_artist,
44
+ "track_title": input_title,
45
+ "extractor_key": "flacfetch",
46
+ "id": f"flacfetch_{input_artist}_{input_title}".replace(" ", "_"),
47
+ "url": None, # URL will be determined by flacfetch during download
48
+ "source": "flacfetch",
49
+ }
50
+
51
+ # No valid input provided
52
+ raise ValueError(
53
+ f"Artist and title are required for audio search. "
54
+ f"Received artist: {input_artist}, title: {input_title}"
55
+ )
56
+
57
+
58
+ def parse_track_metadata(extracted_info, current_artist, current_title, persistent_artist, logger):
59
+ """
60
+ Parses extracted_info to determine URL, extractor, ID, artist, and title.
61
+ Returns a dictionary with the parsed values.
62
+
63
+ This function now supports both legacy yt-dlp style metadata and
64
+ the new flacfetch-based metadata format.
65
+ """
66
+ parsed_data = {
67
+ "url": None,
68
+ "extractor": None,
69
+ "media_id": None,
70
+ "artist": current_artist,
71
+ "title": current_title,
72
+ }
73
+
74
+ metadata_artist = ""
75
+ metadata_title = ""
76
+
77
+ # Handle flacfetch-style metadata (no URL required)
78
+ if extracted_info.get("source") == "flacfetch":
79
+ parsed_data["url"] = None # URL determined at download time
80
+ parsed_data["extractor"] = "flacfetch"
81
+ parsed_data["media_id"] = extracted_info.get("id")
82
+
83
+ # Use the provided artist/title directly
84
+ if extracted_info.get("artist"):
85
+ parsed_data["artist"] = extracted_info["artist"]
86
+ if extracted_info.get("track_title"):
87
+ parsed_data["title"] = extracted_info["track_title"]
88
+
89
+ if persistent_artist:
90
+ parsed_data["artist"] = persistent_artist
91
+
92
+ logger.info(f"Using flacfetch metadata: artist: {parsed_data['artist']}, title: {parsed_data['title']}")
93
+ return parsed_data
94
+
95
+ # Legacy yt-dlp style metadata handling (for backward compatibility)
96
+ if "url" in extracted_info:
97
+ parsed_data["url"] = extracted_info["url"]
98
+ elif "webpage_url" in extracted_info:
99
+ parsed_data["url"] = extracted_info["webpage_url"]
100
+ else:
101
+ # For flacfetch results without URL, this is now acceptable
102
+ logger.debug("No URL in extracted info - will be determined at download time")
103
+ parsed_data["url"] = None
104
+
105
+ if "extractor_key" in extracted_info:
106
+ parsed_data["extractor"] = extracted_info["extractor_key"]
107
+ elif "ie_key" in extracted_info:
108
+ parsed_data["extractor"] = extracted_info["ie_key"]
109
+ elif extracted_info.get("source") == "flacfetch":
110
+ parsed_data["extractor"] = "flacfetch"
111
+ else:
112
+ # Default to flacfetch if no extractor specified
113
+ parsed_data["extractor"] = "flacfetch"
114
+
115
+ if "id" in extracted_info:
116
+ parsed_data["media_id"] = extracted_info["id"]
117
+
118
+ # Example: "Artist - Title"
119
+ if "title" in extracted_info and "-" in extracted_info["title"]:
120
+ try:
121
+ metadata_artist, metadata_title = extracted_info["title"].split("-", 1)
122
+ metadata_artist = metadata_artist.strip()
123
+ metadata_title = metadata_title.strip()
124
+ except ValueError:
125
+ logger.warning(f"Could not split title '{extracted_info['title']}' on '-', using full title.")
126
+ metadata_title = extracted_info["title"].strip()
127
+ if "uploader" in extracted_info:
128
+ metadata_artist = extracted_info["uploader"]
129
+
130
+ elif "uploader" in extracted_info:
131
+ # Fallback to uploader as artist if title parsing fails
132
+ metadata_artist = extracted_info["uploader"]
133
+ if "title" in extracted_info:
134
+ metadata_title = extracted_info["title"].strip()
135
+
136
+ # If unable to parse, log an appropriate message
137
+ if not metadata_artist or not metadata_title:
138
+ logger.warning("Could not parse artist and title from the input media metadata.")
139
+
140
+ if not parsed_data["artist"] and metadata_artist:
141
+ logger.warning(f"Artist not provided as input, setting to {metadata_artist} from input media metadata...")
142
+ parsed_data["artist"] = metadata_artist
143
+
144
+ if not parsed_data["title"] and metadata_title:
145
+ logger.warning(f"Title not provided as input, setting to {metadata_title} from input media metadata...")
146
+ parsed_data["title"] = metadata_title
147
+
148
+ if persistent_artist:
149
+ logger.debug(
150
+ f"Resetting artist from {parsed_data['artist']} to persistent artist: {persistent_artist} for consistency while processing playlist..."
151
+ )
152
+ parsed_data["artist"] = persistent_artist
153
+
154
+ if parsed_data["artist"] and parsed_data["title"]:
155
+ logger.info(f"Parsed metadata - artist: {parsed_data['artist']}, title: {parsed_data['title']}")
156
+ else:
157
+ logger.debug(extracted_info)
158
+ raise Exception("Failed to extract artist and title from the input media metadata.")
159
+
160
+ return parsed_data