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,492 @@
1
+ #!/usr/bin/env python
2
+ # Suppress SyntaxWarnings from third-party dependencies (pydub, syrics)
3
+ # that have invalid escape sequences in regex patterns (not yet fixed for Python 3.12+)
4
+ import warnings
5
+ warnings.filterwarnings("ignore", category=SyntaxWarning, module="pydub")
6
+ warnings.filterwarnings("ignore", category=SyntaxWarning, module="syrics")
7
+
8
+ import argparse
9
+ import logging
10
+ import pkg_resources
11
+ import os
12
+ import csv
13
+ import asyncio
14
+ import json
15
+ import sys
16
+ from karaoke_gen import KaraokePrep
17
+ from karaoke_gen.karaoke_finalise import KaraokeFinalise
18
+
19
+ # Global logger
20
+ logger = logging.getLogger(__name__)
21
+ logger.setLevel(logging.INFO) # Set initial log level
22
+ # Prevent log propagation to root logger to avoid duplicate logs
23
+ # when external packages (like lyrics_converter) configure root logger handlers
24
+ logger.propagate = False
25
+
26
+
27
+ async def process_track_prep(row, args, logger, log_formatter):
28
+ """First phase: Process a track through prep stage only, without video rendering"""
29
+ original_dir = os.getcwd()
30
+ try:
31
+ artist = row["Artist"].strip()
32
+ title = row["Title"].strip()
33
+ guide_file = row["Mixed Audio Filename"].strip()
34
+ instrumental_file = row["Instrumental Audio Filename"].strip()
35
+
36
+ logger.info(f"Initial prep phase for track: {artist} - {title}")
37
+
38
+ kprep = KaraokePrep(
39
+ artist=artist,
40
+ title=title,
41
+ input_media=guide_file,
42
+ existing_instrumental=instrumental_file,
43
+ style_params_json=args.style_params_json,
44
+ logger=logger,
45
+ log_level=args.log_level,
46
+ dry_run=args.dry_run,
47
+ render_video=False, # First phase: no video rendering
48
+ create_track_subfolders=True,
49
+ )
50
+
51
+ tracks = await kprep.process()
52
+ return True
53
+ except Exception as e:
54
+ logger.error(f"Failed initial prep for {artist} - {title}: {str(e)}")
55
+ return False
56
+ finally:
57
+ os.chdir(original_dir)
58
+
59
+
60
+ async def process_track_render(row, args, logger, log_formatter):
61
+ """Phase 2: Process a track through karaoke-finalise."""
62
+ # First, load CDG styles if CDG generation is enabled
63
+ cdg_styles = None
64
+ if args.enable_cdg:
65
+ if not args.style_params_json:
66
+ # Raise ValueError instead of sys.exit
67
+ raise ValueError("CDG styles JSON file path (--style_params_json) is required when --enable_cdg is used")
68
+ try:
69
+ with open(args.style_params_json, "r") as f:
70
+ style_params = json.load(f) # Use json.load directly with file object
71
+ # Check if 'cdg' key exists
72
+ if "cdg" not in style_params:
73
+ raise ValueError(f"'cdg' key not found in style parameters file: {args.style_params_json}")
74
+ cdg_styles = style_params["cdg"]
75
+ except FileNotFoundError:
76
+ # Re-raise FileNotFoundError
77
+ raise FileNotFoundError(f"CDG styles configuration file not found: {args.style_params_json}")
78
+ except json.JSONDecodeError as e:
79
+ # Raise ValueError for invalid JSON
80
+ raise ValueError(f"Invalid JSON in CDG styles configuration file: {str(e)}")
81
+
82
+ original_dir = os.getcwd()
83
+ artist = row["Artist"].strip()
84
+ title = row["Title"].strip()
85
+ guide_file = row["Mixed Audio Filename"].strip()
86
+ instrumental_file = row["Instrumental Audio Filename"].strip()
87
+
88
+ try:
89
+ # Initialize KaraokeFinalise first (needed for test assertions)
90
+ kfinalise = KaraokeFinalise(
91
+ log_formatter=log_formatter,
92
+ log_level=args.log_level,
93
+ dry_run=args.dry_run,
94
+ enable_cdg=args.enable_cdg,
95
+ enable_txt=args.enable_txt,
96
+ cdg_styles=cdg_styles,
97
+ non_interactive=True
98
+ )
99
+
100
+ # Try to find the track directory
101
+ track_dir_found = False
102
+
103
+ # Try several directory naming patterns
104
+ possible_dirs = [
105
+ os.path.join(args.output_dir, f"{artist} - {title}"),
106
+ os.path.join(args.output_dir, f"{artist} - {title}"), # Original artist/title from row
107
+ os.path.join(args.output_dir, f"{artist} - {title}") # With space replace (same here)
108
+ ]
109
+
110
+ for track_dir in possible_dirs:
111
+ if os.path.exists(track_dir):
112
+ track_dir_found = True
113
+ break
114
+
115
+ if not track_dir_found:
116
+ logger.error(f"Track directory not found. Tried: {', '.join(possible_dirs)}")
117
+ return True # Return True to continue with other tracks
118
+
119
+ # First run KaraokePrep with video rendering enabled
120
+ # This is so the human can review all of the lyrics for the entire batch fairly quickly,
121
+ # then leave the script running to render the videos for all of them.
122
+ logger.info(f"Video rendering for track: {artist} - {title}")
123
+ kprep = KaraokePrep(
124
+ artist=artist,
125
+ title=title,
126
+ input_media=guide_file,
127
+ existing_instrumental=instrumental_file,
128
+ style_params_json=args.style_params_json,
129
+ logger=logger,
130
+ log_level=args.log_level,
131
+ dry_run=args.dry_run,
132
+ render_video=True, # Second phase: with video rendering
133
+ create_track_subfolders=True,
134
+ skip_transcription_review=True,
135
+ )
136
+
137
+ tracks = await kprep.process()
138
+
139
+ # Process with KaraokeFinalise in the track directory
140
+ for track_dir in possible_dirs:
141
+ if os.path.exists(track_dir):
142
+ try:
143
+ os.chdir(track_dir)
144
+ # Process with KaraokeFinalise
145
+ kfinalise.process()
146
+ return True
147
+ except Exception as e:
148
+ logger.error(f"Error during finalisation: {str(e)}")
149
+ raise # Re-raise to be caught by outer try/except
150
+ finally:
151
+ # Always go back to original directory
152
+ os.chdir(original_dir)
153
+
154
+ except Exception as e:
155
+ logger.error(f"Failed render/finalise for {artist} - {title}: {str(e)}")
156
+ os.chdir(original_dir) # Make sure we go back to original directory
157
+ return False
158
+
159
+
160
+ def update_csv_status(csv_path, row_index, new_status, dry_run=False):
161
+ """Update the status of a processed row in the CSV file.
162
+
163
+ Args:
164
+ csv_path (str): Path to the CSV file
165
+ row_index (int): Index of the row to update
166
+ new_status (str): New status to set
167
+ dry_run (bool): If True, log the update but don't modify the file
168
+
169
+ Returns:
170
+ bool: True if updated, False if in dry run mode or error occurred
171
+ """
172
+ if dry_run:
173
+ logger.info(f"DRY RUN: Would update row {row_index} in {csv_path} to status '{new_status}'")
174
+ return False
175
+
176
+ try:
177
+ # Read all rows
178
+ with open(csv_path, "r") as f:
179
+ reader = csv.DictReader(f)
180
+ rows = list(reader)
181
+
182
+ # Check if CSV has any rows
183
+ if not rows:
184
+ logger.error(f"CSV file {csv_path} is empty or has no data rows")
185
+ return False
186
+
187
+ # Update status for the processed row
188
+ if row_index < 0 or row_index >= len(rows):
189
+ logger.error(f"Row index {row_index} is out of range for CSV with {len(rows)} rows")
190
+ return False
191
+
192
+ rows[row_index]["Status"] = new_status
193
+
194
+ # Write back to CSV
195
+ fieldnames = rows[0].keys()
196
+ with open(csv_path, "w", newline="") as f:
197
+ writer = csv.DictWriter(f, fieldnames=fieldnames)
198
+ writer.writeheader()
199
+ writer.writerows(rows)
200
+
201
+ return True
202
+
203
+ except Exception as e:
204
+ logger.error(f"Error updating CSV status: {str(e)}")
205
+ return False
206
+
207
+
208
+ def parse_arguments():
209
+ """Parse command line arguments"""
210
+ parser = argparse.ArgumentParser(
211
+ description="Process multiple karaoke tracks in bulk from a CSV file.",
212
+ formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=54),
213
+ )
214
+
215
+ # Basic information
216
+ parser.add_argument(
217
+ "input_csv",
218
+ help="Path to CSV file containing tracks to process. CSV should have columns: Artist,Title,Mixed Audio Filename,Instrumental Audio Filename,Status",
219
+ )
220
+
221
+ package_version = pkg_resources.get_distribution("karaoke-gen").version
222
+ parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {package_version}")
223
+
224
+ # Required arguments
225
+ parser.add_argument(
226
+ "--style_params_json",
227
+ required=True,
228
+ help="Path to style parameters JSON file",
229
+ )
230
+ parser.add_argument(
231
+ "--output_dir",
232
+ default=".",
233
+ help="Optional: directory to write output files (default: <current dir>). Example: --output_dir=/app/karaoke",
234
+ )
235
+
236
+ # Finalise-specific arguments
237
+ parser.add_argument(
238
+ "--enable_cdg",
239
+ action="store_true",
240
+ help="Optional: Enable CDG ZIP generation during finalisation. Example: --enable_cdg",
241
+ )
242
+ parser.add_argument(
243
+ "--enable_txt",
244
+ action="store_true",
245
+ help="Optional: Enable TXT ZIP generation during finalisation. Example: --enable_txt",
246
+ )
247
+
248
+ # Logging & Debugging
249
+ parser.add_argument(
250
+ "--log_level",
251
+ default="info",
252
+ help="Optional: logging level, e.g. info, debug, warning (default: %(default)s). Example: --log_level=debug",
253
+ )
254
+ parser.add_argument(
255
+ "--dry_run",
256
+ action="store_true",
257
+ help="Optional: perform a dry run without making any changes (default: %(default)s). Example: --dry_run",
258
+ )
259
+
260
+ args = parser.parse_args()
261
+
262
+ # Convert input_csv to absolute path early
263
+ args.input_csv = os.path.abspath(args.input_csv)
264
+
265
+ # Validate and convert log level
266
+ if isinstance(args.log_level, str):
267
+ try:
268
+ log_level_int = getattr(logging, args.log_level.upper())
269
+ args.log_level = log_level_int # Store the numeric log level back in args
270
+ except AttributeError:
271
+ # Raise ValueError for invalid log level string
272
+ raise ValueError(f"Invalid log level string: {args.log_level}")
273
+ elif not isinstance(args.log_level, int):
274
+ # If it's neither string nor int, raise error
275
+ raise ValueError(f"Invalid log level type: {type(args.log_level)}")
276
+
277
+ return args
278
+
279
+
280
+ def _parse_and_validate_args():
281
+ """Parses arguments and performs initial validation."""
282
+ args = parse_arguments() # Calls the modified parse_arguments
283
+
284
+ # Validate input CSV existence (raises FileNotFoundError if invalid)
285
+ if not validate_input_csv(args.input_csv):
286
+ raise FileNotFoundError(f"Input CSV file not found: {args.input_csv}")
287
+
288
+ # Validate style params JSON existence if CDG is enabled
289
+ if args.enable_cdg:
290
+ if not args.style_params_json:
291
+ raise ValueError("CDG styles JSON file path (--style_params_json) is required when --enable_cdg is used")
292
+ if not os.path.isfile(args.style_params_json):
293
+ raise FileNotFoundError(f"CDG styles configuration file not found: {args.style_params_json}")
294
+ # Basic JSON validation can also happen here if desired, or deferred to process_track_render
295
+ try:
296
+ with open(args.style_params_json, 'r') as f:
297
+ json.load(f)
298
+ except json.JSONDecodeError as e:
299
+ raise ValueError(f"Invalid JSON in CDG styles configuration file: {args.style_params_json} - {e}")
300
+ except FileNotFoundError: # Should be caught above, but belt-and-suspenders
301
+ raise FileNotFoundError(f"CDG styles configuration file not found: {args.style_params_json}")
302
+
303
+ return args
304
+
305
+
306
+ def validate_input_csv(csv_path):
307
+ """Validate that the input CSV file exists.
308
+
309
+ Args:
310
+ csv_path (str): Path to the CSV file
311
+
312
+ Returns:
313
+ bool: True if the file exists, False otherwise
314
+ """
315
+ if not os.path.isfile(csv_path):
316
+ logger.error(f"Input CSV file not found: {csv_path}")
317
+ return False
318
+ return True
319
+
320
+
321
+ def _read_csv_file(csv_path):
322
+ """Reads the CSV file and returns rows as a list of dictionaries."""
323
+ try:
324
+ with open(csv_path, "r", newline='') as f: # Added newline=''
325
+ reader = csv.DictReader(f)
326
+ # Check for required columns before reading all rows
327
+ required_columns = {"Artist", "Title", "Mixed Audio Filename", "Instrumental Audio Filename", "Status"}
328
+ if not required_columns.issubset(reader.fieldnames):
329
+ missing = required_columns - set(reader.fieldnames)
330
+ raise ValueError(f"CSV file missing required columns: {', '.join(missing)}")
331
+ rows = list(reader)
332
+ if not rows:
333
+ logger.warning(f"CSV file {csv_path} is empty or contains only headers.")
334
+ return rows
335
+ except FileNotFoundError:
336
+ # This should ideally be caught earlier by validate_input_csv, but handle defensively
337
+ logger.error(f"CSV file not found during read: {csv_path}")
338
+ raise # Re-raise the exception
339
+ except Exception as e:
340
+ logger.error(f"Error reading CSV file {csv_path}: {e}")
341
+ raise # Re-raise other read errors
342
+
343
+
344
+ async def process_csv_rows(csv_path, rows, args, logger, log_formatter):
345
+ """Process all rows in a CSV file.
346
+
347
+ Args:
348
+ csv_path (str): Path to the CSV file
349
+ rows (list): List of CSV rows as dictionaries
350
+ args (argparse.Namespace): Command line arguments
351
+ logger (logging.Logger): Logger instance
352
+ log_formatter (logging.Formatter): Log formatter
353
+
354
+ Returns:
355
+ dict: A summary of the processing results
356
+ """
357
+ results = {
358
+ "prep_success": 0,
359
+ "prep_failed": 0,
360
+ "render_success": 0,
361
+ "render_failed": 0,
362
+ "skipped": 0
363
+ }
364
+
365
+ # Phase 1: Initial prep for all tracks
366
+ logger.info("Starting Phase 1: Initial prep for all tracks")
367
+ for i, row in enumerate(rows):
368
+ status = row["Status"].lower() if "Status" in row else ""
369
+ if status != "uploaded":
370
+ logger.info(f"Skipping {row.get('Artist', 'Unknown')} - {row.get('Title', 'Unknown')} (Status: {row.get('Status', 'Unknown')})")
371
+ results["skipped"] += 1
372
+ continue
373
+
374
+ success = await process_track_prep(row, args, logger, log_formatter)
375
+ if success:
376
+ results["prep_success"] += 1
377
+ if not args.dry_run:
378
+ update_csv_status(csv_path, i, "Prep_Complete", args.dry_run)
379
+ else:
380
+ results["prep_failed"] += 1
381
+ if not args.dry_run:
382
+ update_csv_status(csv_path, i, "Prep_Failed", args.dry_run)
383
+
384
+ # Phase 2: Render and finalise all tracks
385
+ logger.info("Starting Phase 2: Render and finalise for all tracks")
386
+ for i, row in enumerate(rows):
387
+ status = row["Status"].lower() if "Status" in row else ""
388
+ if status not in ["prep_complete", "uploaded"]:
389
+ logger.info(f"Skipping {row.get('Artist', 'Unknown')} - {row.get('Title', 'Unknown')} (Status: {row.get('Status', 'Unknown')})")
390
+ continue
391
+
392
+ success = await process_track_render(row, args, logger, log_formatter)
393
+ if success:
394
+ results["render_success"] += 1
395
+ if not args.dry_run:
396
+ update_csv_status(csv_path, i, "Completed", args.dry_run)
397
+ else:
398
+ results["render_failed"] += 1
399
+ if not args.dry_run:
400
+ update_csv_status(csv_path, i, "Render_Failed", args.dry_run)
401
+
402
+ return results
403
+
404
+
405
+ async def async_main():
406
+ """Main async function to process bulk tracks from CSV"""
407
+ # Parse and validate arguments first (raises exceptions on failure)
408
+ args = _parse_and_validate_args()
409
+
410
+ # Set log level based on validated args (logger should already be partially set up by main)
411
+ logger.setLevel(args.log_level)
412
+ logger.info(f"Log level set to {logging.getLevelName(args.log_level)}")
413
+ if args.dry_run:
414
+ logger.info("Dry run mode enabled. No changes will be made.")
415
+
416
+ logger.info(f"Starting bulk processing with input CSV: {args.input_csv}")
417
+
418
+ # Read CSV (raises exceptions on failure)
419
+ rows = _read_csv_file(args.input_csv)
420
+
421
+ # Check if log_formatter is available (should be set by main)
422
+ global log_formatter
423
+ if log_formatter is None:
424
+ # This case should ideally not happen if main() calls setup_logging correctly
425
+ logger.warning("Log formatter not found, setting up default.")
426
+ log_formatter = setup_logging(args.log_level)
427
+
428
+
429
+ # Process the CSV rows
430
+ results = await process_csv_rows(args.input_csv, rows, args, logger, log_formatter)
431
+
432
+ # Log summary
433
+ logger.info(f"Processing complete. Summary: {results}")
434
+ return results
435
+
436
+
437
+ def setup_logging(log_level=logging.INFO):
438
+ """Set up logging with the given log level.
439
+
440
+ Args:
441
+ log_level (int): Logging level (e.g., logging.INFO, logging.DEBUG)
442
+
443
+ Returns:
444
+ logging.Formatter: The log formatter for use by other functions
445
+ """
446
+ global log_formatter # Make log_formatter accessible to other functions
447
+ log_handler = logging.StreamHandler()
448
+ log_formatter = logging.Formatter(fmt="%(asctime)s.%(msecs)03d - %(levelname)s - %(module)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
449
+ log_handler.setFormatter(log_formatter)
450
+ logger.addHandler(log_handler)
451
+ logger.setLevel(log_level)
452
+ return log_formatter
453
+
454
+
455
+ def main():
456
+ """Main entry point for the CLI."""
457
+ log_formatter = None # Initialize log_formatter
458
+ try:
459
+ # Set up logging early to capture potential errors during setup/parsing
460
+ # Get initial args just for log level if provided, otherwise default
461
+ temp_args, _ = argparse.ArgumentParser(add_help=False).parse_known_args()
462
+ initial_log_level_str = getattr(temp_args, 'log_level', 'info')
463
+ try:
464
+ initial_log_level = getattr(logging, initial_log_level_str.upper())
465
+ except AttributeError:
466
+ initial_log_level = logging.INFO
467
+ print(f"Warning: Invalid initial log level '{initial_log_level_str}'. Using INFO.", file=sys.stderr)
468
+
469
+ log_formatter = setup_logging(initial_log_level)
470
+
471
+ # Run the async main function using asyncio
472
+ asyncio.run(async_main())
473
+ logger.info("Bulk processing finished successfully.")
474
+ sys.exit(0)
475
+ except (FileNotFoundError, ValueError, argparse.ArgumentError) as e:
476
+ # Log specific configuration/setup errors before exiting
477
+ if logger.handlers: # Check if logger was set up
478
+ logger.error(f"Configuration error: {str(e)}")
479
+ else: # Fallback if logging setup failed
480
+ print(f"Error: {str(e)}", file=sys.stderr)
481
+ sys.exit(1)
482
+ except Exception as e:
483
+ # Catch any other unexpected errors during processing
484
+ if logger.handlers:
485
+ logger.exception(f"An unexpected error occurred during bulk processing: {str(e)}") # Use exception for traceback
486
+ else:
487
+ print(f"An unexpected error occurred: {str(e)}", file=sys.stderr)
488
+ sys.exit(1)
489
+
490
+
491
+ if __name__ == "__main__":
492
+ main()