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
@@ -46,6 +46,7 @@ class KaraokeFinalise:
46
46
  non_interactive=False,
47
47
  user_youtube_credentials=None, # Add support for pre-stored credentials
48
48
  server_side_mode=False, # New parameter for server-side deployment
49
+ selected_instrumental_file=None, # Add support for pre-selected instrumental file
49
50
  ):
50
51
  self.log_level = log_level
51
52
  self.log_formatter = log_formatter
@@ -103,6 +104,7 @@ class KaraokeFinalise:
103
104
  self.non_interactive = non_interactive
104
105
  self.user_youtube_credentials = user_youtube_credentials
105
106
  self.server_side_mode = server_side_mode
107
+ self.selected_instrumental_file = selected_instrumental_file
106
108
 
107
109
  self.suffixes = {
108
110
  "title_mov": " (Title).mov",
@@ -151,7 +153,8 @@ class KaraokeFinalise:
151
153
  self.ffmpeg_base_command += " -y"
152
154
 
153
155
  # Detect and configure hardware acceleration
154
- self.nvenc_available = self.detect_nvenc_support()
156
+ # TODO: Re-enable this once we figure out why the resulting MP4s are 10x larger than when encoded with x264...
157
+ self.nvenc_available = False # self.detect_nvenc_support()
155
158
  self.configure_hardware_acceleration()
156
159
 
157
160
  def check_input_files_exist(self, base_name, with_vocals_file, instrumental_audio_file):
@@ -254,9 +257,19 @@ class KaraokeFinalise:
254
257
 
255
258
  self.logger.debug(f"YouTube upload checks passed, enabling YouTube upload")
256
259
  self.youtube_upload_enabled = True
260
+
261
+ # Also enable YouTube upload if pre-stored credentials are provided (server-side mode)
262
+ elif self.user_youtube_credentials is not None and self.youtube_description_file is not None:
263
+ if not os.path.isfile(self.youtube_description_file):
264
+ raise Exception(f"YouTube description file does not exist: {self.youtube_description_file}")
265
+
266
+ self.logger.debug(f"Pre-stored YouTube credentials provided, enabling YouTube upload")
267
+ self.youtube_upload_enabled = True
257
268
 
258
269
  # Enable discord notifications if webhook URL is provided and is valid URL
259
270
  if self.discord_webhook_url is not None:
271
+ # Strip whitespace/newlines that may have been introduced from environment variables or secrets
272
+ self.discord_webhook_url = self.discord_webhook_url.strip()
260
273
  if not self.discord_webhook_url.startswith("https://discord.com/api/webhooks/"):
261
274
  raise Exception(f"Discord webhook URL is not valid: {self.discord_webhook_url}")
262
275
 
@@ -409,26 +422,33 @@ class KaraokeFinalise:
409
422
  if "items" in response and len(response["items"]) > 0:
410
423
  for item in response["items"]:
411
424
  found_title = item["snippet"]["title"]
412
- similarity_score = fuzz.ratio(youtube_title.lower(), found_title.lower())
413
- if similarity_score >= 70: # 70% similarity
425
+
426
+ # In server-side mode, require an exact match to avoid false positives.
427
+ # Otherwise, use fuzzy matching for interactive CLI usage.
428
+ if self.server_side_mode:
429
+ is_match = youtube_title.lower() == found_title.lower()
430
+ similarity_score = 100 if is_match else 0
431
+ else:
432
+ similarity_score = fuzz.ratio(youtube_title.lower(), found_title.lower())
433
+ is_match = similarity_score >= 70
434
+
435
+ if is_match:
414
436
  found_id = item["id"]["videoId"]
415
437
  self.logger.info(
416
438
  f"Potential match found on YouTube channel with ID: {found_id} and title: {found_title} (similarity: {similarity_score}%)"
417
439
  )
418
-
419
- # In non-interactive mode, automatically confirm if similarity is high enough
440
+
441
+ # In non-interactive mode (server mode), we don't prompt. Just record the match and return.
420
442
  if self.non_interactive:
421
- self.logger.info(f"Non-interactive mode, automatically confirming match with similarity score {similarity_score}%")
443
+ self.logger.info(f"Non-interactive mode, found a match.")
422
444
  self.youtube_video_id = found_id
423
445
  self.youtube_url = f"{self.youtube_url_prefix}{self.youtube_video_id}"
424
- self.skip_notifications = True
425
446
  return True
426
-
447
+
427
448
  confirmation = input(f"Is '{found_title}' the video you are finalising? (y/n): ").strip().lower()
428
449
  if confirmation == "y":
429
450
  self.youtube_video_id = found_id
430
451
  self.youtube_url = f"{self.youtube_url_prefix}{self.youtube_video_id}"
431
- self.skip_notifications = True
432
452
  return True
433
453
 
434
454
  self.logger.info(f"No matching video found with title: {youtube_title}")
@@ -480,8 +500,12 @@ class KaraokeFinalise:
480
500
  max_length = 95
481
501
  youtube_title = self.truncate_to_nearest_word(youtube_title, max_length)
482
502
 
503
+ # In server-side mode, we should always replace videos if an exact match is found.
504
+ # Otherwise, respect the replace_existing flag from CLI.
505
+ should_replace = True if self.server_side_mode else replace_existing
506
+
483
507
  if self.check_if_video_title_exists_on_youtube_channel(youtube_title):
484
- if replace_existing:
508
+ if should_replace:
485
509
  self.logger.info(f"Video already exists on YouTube, deleting before re-upload: {self.youtube_url}")
486
510
  if self.delete_youtube_video(self.youtube_video_id):
487
511
  self.logger.info(f"Successfully deleted existing video, proceeding with upload")
@@ -530,10 +554,17 @@ class KaraokeFinalise:
530
554
  self.logger.info(f"Uploaded video to YouTube: {self.youtube_url}")
531
555
 
532
556
  # Uploading the thumbnail
533
- if input_files["title_jpg"]:
534
- media_thumbnail = MediaFileUpload(input_files["title_jpg"], mimetype="image/jpeg")
535
- youtube.thumbnails().set(videoId=self.youtube_video_id, media_body=media_thumbnail).execute()
536
- self.logger.info(f"Uploaded thumbnail for video ID {self.youtube_video_id}")
557
+ if input_files.get("title_jpg") and os.path.isfile(input_files["title_jpg"]):
558
+ try:
559
+ self.logger.info(f"Uploading thumbnail from: {input_files['title_jpg']}")
560
+ media_thumbnail = MediaFileUpload(input_files["title_jpg"], mimetype="image/jpeg")
561
+ youtube.thumbnails().set(videoId=self.youtube_video_id, media_body=media_thumbnail).execute()
562
+ self.logger.info(f"Uploaded thumbnail for video ID {self.youtube_video_id}")
563
+ except Exception as e:
564
+ self.logger.error(f"Failed to upload thumbnail: {e}")
565
+ self.logger.warning("Video uploaded but thumbnail not set. You may need to set it manually on YouTube.")
566
+ else:
567
+ self.logger.warning(f"Thumbnail file not found, skipping thumbnail upload: {input_files.get('title_jpg')}")
537
568
 
538
569
  def get_next_brand_code(self):
539
570
  """
@@ -746,13 +777,13 @@ class KaraokeFinalise:
746
777
  # Hardware-accelerated version
747
778
  gpu_command = (
748
779
  f'{self.ffmpeg_base_command} {self.hwaccel_decode_flags} -i "{input_file}" '
749
- f'-c:v {self.video_encoder} {self.get_nvenc_quality_settings("high")} -c:a {self.aac_codec} {self.mp4_flags} "{output_file}"'
780
+ f'-c:v {self.video_encoder} {self.get_nvenc_quality_settings("high")} -c:a {self.aac_codec} -ar 48000 {self.mp4_flags} "{output_file}"'
750
781
  )
751
782
 
752
783
  # Software fallback version
753
784
  cpu_command = (
754
785
  f'{self.ffmpeg_base_command} -i "{input_file}" '
755
- f'-c:v libx264 -c:a {self.aac_codec} {self.mp4_flags} "{output_file}"'
786
+ f'-c:v libx264 -c:a {self.aac_codec} -ar 48000 {self.mp4_flags} "{output_file}"'
756
787
  )
757
788
 
758
789
  self.execute_command_with_fallback(gpu_command, cpu_command, "Converting MOV video to MP4")
@@ -782,7 +813,7 @@ class KaraokeFinalise:
782
813
  # Hardware acceleration doesn't provide significant benefit for copy operations
783
814
  ffmpeg_command = (
784
815
  f'{self.ffmpeg_base_command} -i "{input_file}" '
785
- f'-c:v copy -c:a {self.aac_codec} -b:a 320k {self.mp4_flags} "{output_file}"'
816
+ f'-c:v copy -c:a {self.aac_codec} -ar 48000 -b:a 320k {self.mp4_flags} "{output_file}"'
786
817
  )
787
818
  self.execute_command(ffmpeg_command, "Creating MP4 version with AAC audio")
788
819
 
@@ -803,14 +834,14 @@ class KaraokeFinalise:
803
834
  f'{self.ffmpeg_base_command} {self.hwaccel_decode_flags} -i "{input_file}" '
804
835
  f'-c:v {self.video_encoder} -vf "{self.scale_filter}=1280:720" '
805
836
  f'{self.get_nvenc_quality_settings("medium")} -b:v 2000k '
806
- f'-c:a {self.aac_codec} -b:a 128k {self.mp4_flags} "{output_file}"'
837
+ f'-c:a {self.aac_codec} -ar 48000 -b:a 128k {self.mp4_flags} "{output_file}"'
807
838
  )
808
839
 
809
840
  # Software fallback version
810
841
  cpu_command = (
811
842
  f'{self.ffmpeg_base_command} -i "{input_file}" '
812
843
  f'-c:v libx264 -vf "scale=1280:720" -b:v 2000k -preset medium -tune animation '
813
- f'-c:a {self.aac_codec} -b:a 128k {self.mp4_flags} "{output_file}"'
844
+ f'-c:a {self.aac_codec} -ar 48000 -b:a 128k {self.mp4_flags} "{output_file}"'
814
845
  )
815
846
 
816
847
  self.execute_command_with_fallback(gpu_command, cpu_command, "Encoding 720p version of the final video")
@@ -829,7 +860,7 @@ class KaraokeFinalise:
829
860
  return env_mov_input, ffmpeg_filter
830
861
 
831
862
  def remux_and_encode_output_video_files(self, with_vocals_file, input_files, output_files):
832
- self.logger.info(f"Remuxing and encoding output video files...")
863
+ self.logger.info(f"Remuxing and encoding output video files (4 formats, ~15-20 minutes total)...")
833
864
 
834
865
  # Check if output files already exist
835
866
  if os.path.isfile(output_files["final_karaoke_lossless_mp4"]) and os.path.isfile(output_files["final_karaoke_lossless_mkv"]):
@@ -840,16 +871,20 @@ class KaraokeFinalise:
840
871
  return
841
872
 
842
873
  # Create karaoke version with instrumental audio
874
+ self.logger.info(f"[Step 1/6] Remuxing video with instrumental audio...")
843
875
  self.remux_with_instrumental(with_vocals_file, input_files["instrumental_audio"], output_files["karaoke_mp4"])
844
876
 
845
877
  # Convert the with vocals video to MP4 if needed
846
878
  if not with_vocals_file.endswith(".mp4"):
879
+ self.logger.info(f"[Step 2/6] Converting karaoke video to MP4...")
847
880
  self.convert_mov_to_mp4(with_vocals_file, output_files["with_vocals_mp4"])
848
881
 
849
882
  # Delete the with vocals mov after successfully converting it to mp4
850
883
  if not self.dry_run and os.path.isfile(with_vocals_file):
851
884
  self.logger.info(f"Deleting with vocals MOV file: {with_vocals_file}")
852
885
  os.remove(with_vocals_file)
886
+ else:
887
+ self.logger.info(f"[Step 2/6] Skipped - video already in MP4 format")
853
888
 
854
889
  # Quote file paths to handle special characters
855
890
  title_mov_file = shlex.quote(os.path.abspath(input_files["title_mov"]))
@@ -858,10 +893,17 @@ class KaraokeFinalise:
858
893
  # Prepare concat filter for combining videos
859
894
  env_mov_input, ffmpeg_filter = self.prepare_concat_filter(input_files)
860
895
 
861
- # Create all output versions
896
+ # Create all output versions with progress logging
897
+ self.logger.info(f"[Step 3/6] Encoding lossless 4K MP4 (title + karaoke + end, ~5 minutes)...")
862
898
  self.encode_lossless_mp4(title_mov_file, karaoke_mp4_file, env_mov_input, ffmpeg_filter, output_files["final_karaoke_lossless_mp4"])
899
+
900
+ self.logger.info(f"[Step 4/6] Encoding lossy 4K MP4 with AAC audio (~1 minute)...")
863
901
  self.encode_lossy_mp4(output_files["final_karaoke_lossless_mp4"], output_files["final_karaoke_lossy_mp4"])
902
+
903
+ self.logger.info(f"[Step 5/6] Creating MKV with FLAC audio for YouTube (~1 minute)...")
864
904
  self.encode_lossless_mkv(output_files["final_karaoke_lossless_mp4"], output_files["final_karaoke_lossless_mkv"])
905
+
906
+ self.logger.info(f"[Step 6/6] Encoding 720p version (~3 minutes)...")
865
907
  self.encode_720p_version(output_files["final_karaoke_lossless_mp4"], output_files["final_karaoke_lossy_720p_mp4"])
866
908
 
867
909
  # Skip user confirmation in non-interactive mode for Modal deployment
@@ -1064,7 +1106,7 @@ class KaraokeFinalise:
1064
1106
  os.remove(file_path)
1065
1107
  self.logger.info(f"Deleted .DS_Store file: {file_path}")
1066
1108
 
1067
- rclone_cmd = f"rclone copy -v {shlex.quote(self.public_share_dir)} {shlex.quote(self.rclone_destination)}"
1109
+ rclone_cmd = f"rclone copy -v --ignore-existing {shlex.quote(self.public_share_dir)} {shlex.quote(self.rclone_destination)}"
1068
1110
  self.execute_command(rclone_cmd, "Copying to cloud destination")
1069
1111
 
1070
1112
  def post_discord_notification(self):
@@ -1073,6 +1115,11 @@ class KaraokeFinalise:
1073
1115
  if self.skip_notifications:
1074
1116
  self.logger.info(f"Skipping Discord notification as video was previously uploaded to YouTube")
1075
1117
  return
1118
+
1119
+ # Only post if we have a YouTube URL
1120
+ if not self.youtube_url:
1121
+ self.logger.info(f"Skipping Discord notification - no YouTube URL available")
1122
+ return
1076
1123
 
1077
1124
  if self.dry_run:
1078
1125
  self.logger.info(
@@ -1189,7 +1236,7 @@ class KaraokeFinalise:
1189
1236
  current_dir = os.getcwd()
1190
1237
 
1191
1238
  # Use rclone copy to upload the entire current directory to the remote destination
1192
- rclone_upload_cmd = f"rclone copy -v {shlex.quote(current_dir)} {shlex.quote(remote_dest)}"
1239
+ rclone_upload_cmd = f"rclone copy -v --ignore-existing {shlex.quote(current_dir)} {shlex.quote(remote_dest)}"
1193
1240
 
1194
1241
  if self.dry_run:
1195
1242
  self.logger.info(f"DRY RUN: Would upload current directory to: {remote_dest}")
@@ -1254,14 +1301,23 @@ class KaraokeFinalise:
1254
1301
  self.upload_final_mp4_to_youtube_with_title_thumbnail(artist, title, input_files, output_files, replace_existing)
1255
1302
  except Exception as e:
1256
1303
  self.logger.error(f"Failed to upload video to YouTube: {e}")
1257
- print("Please manually upload the video to YouTube.")
1258
- print()
1259
- self.youtube_video_id = input("Enter the manually uploaded YouTube video ID: ").strip()
1260
- self.youtube_url = f"{self.youtube_url_prefix}{self.youtube_video_id}"
1261
- self.logger.info(f"Using manually provided YouTube video ID: {self.youtube_video_id}")
1304
+ if not self.non_interactive:
1305
+ print("Please manually upload the video to YouTube.")
1306
+ print()
1307
+ self.youtube_video_id = input("Enter the manually uploaded YouTube video ID: ").strip()
1308
+ self.youtube_url = f"{self.youtube_url_prefix}{self.youtube_video_id}"
1309
+ self.logger.info(f"Using manually provided YouTube video ID: {self.youtube_video_id}")
1310
+ else:
1311
+ self.logger.error("YouTube upload failed in non-interactive mode, skipping")
1262
1312
 
1263
- if self.discord_notication_enabled:
1313
+ # Discord notification - runs independently of YouTube upload
1314
+ # Wrapped in try/except so failures don't crash the entire job
1315
+ if self.discord_notication_enabled:
1316
+ try:
1264
1317
  self.post_discord_notification()
1318
+ except Exception as e:
1319
+ self.logger.error(f"Failed to send Discord notification: {e}")
1320
+ self.logger.warning("Continuing without Discord notification - this is non-fatal")
1265
1321
 
1266
1322
  # Handle folder organization - different logic for server-side vs local mode
1267
1323
  if self.server_side_mode and self.brand_prefix and self.organised_dir_rclone_root:
@@ -1362,6 +1418,12 @@ class KaraokeFinalise:
1362
1418
  self.logger.info("Email template file not provided, skipping email draft creation.")
1363
1419
  return
1364
1420
 
1421
+ if not self.youtube_client_secrets_file:
1422
+ self.logger.error("Email template file was provided, but youtube_client_secrets_file is required for Gmail authentication.")
1423
+ self.logger.error("Please provide --youtube_client_secrets_file parameter to enable email draft creation.")
1424
+ self.logger.info("Skipping email draft creation.")
1425
+ return
1426
+
1365
1427
  with open(self.email_template_file, "r") as f:
1366
1428
  template = f.read()
1367
1429
 
@@ -1416,89 +1478,87 @@ class KaraokeFinalise:
1416
1478
  return "aac"
1417
1479
 
1418
1480
  def detect_nvenc_support(self):
1419
- """Detect if NVENC hardware encoding is available with comprehensive checks."""
1481
+ """Detect if NVENC hardware encoding is available."""
1420
1482
  try:
1421
- self.logger.info("🔍 Detecting NVENC hardware acceleration support...")
1483
+ self.logger.info("🔍 Detecting NVENC hardware acceleration...")
1422
1484
 
1423
1485
  if self.dry_run:
1424
- self.logger.info("DRY RUN: Assuming NVENC is available")
1486
+ self.logger.info(" DRY RUN: Assuming NVENC is available")
1425
1487
  return True
1426
1488
 
1427
1489
  import subprocess
1428
1490
  import os
1429
1491
  import shutil
1430
1492
 
1431
- # Step 1: Check for nvidia-smi (indicates NVIDIA driver presence)
1493
+ # Check for nvidia-smi (indicates NVIDIA driver presence)
1432
1494
  try:
1433
1495
  nvidia_smi_result = subprocess.run(["nvidia-smi", "--query-gpu=name,driver_version", "--format=csv,noheader"],
1434
1496
  capture_output=True, text=True, timeout=10)
1435
1497
  if nvidia_smi_result.returncode == 0:
1436
1498
  gpu_info = nvidia_smi_result.stdout.strip()
1437
- self.logger.info(f"✓ NVIDIA GPU detected: {gpu_info}")
1499
+ self.logger.info(f" ✓ NVIDIA GPU detected: {gpu_info}")
1438
1500
  else:
1439
- self.logger.warning("⚠️ nvidia-smi not available or no NVIDIA GPU detected")
1501
+ self.logger.debug(f"nvidia-smi failed: {nvidia_smi_result.stderr}")
1502
+ self.logger.info(" ✗ NVENC not available (no NVIDIA GPU)")
1440
1503
  return False
1441
- except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.CalledProcessError):
1442
- self.logger.warning("⚠️ nvidia-smi not available or failed")
1504
+ except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.CalledProcessError) as e:
1505
+ self.logger.debug(f"nvidia-smi not available: {e}")
1506
+ self.logger.info(" ✗ NVENC not available (no NVIDIA GPU)")
1443
1507
  return False
1444
1508
 
1445
- # Step 2: Check for NVENC encoders in FFmpeg
1509
+ # Check for NVENC encoders in FFmpeg
1446
1510
  try:
1447
1511
  encoders_cmd = f"{self.ffmpeg_base_command} -hide_banner -encoders 2>/dev/null | grep nvenc"
1448
1512
  encoders_result = subprocess.run(encoders_cmd, shell=True, capture_output=True, text=True, timeout=10)
1449
1513
  if encoders_result.returncode == 0 and "nvenc" in encoders_result.stdout:
1450
1514
  nvenc_encoders = [line.strip() for line in encoders_result.stdout.split('\n') if 'nvenc' in line]
1451
- self.logger.info("Found NVENC encoders in FFmpeg:")
1452
- for encoder in nvenc_encoders:
1453
- if encoder:
1454
- self.logger.info(f" {encoder}")
1515
+ self.logger.debug(f"Found NVENC encoders: {nvenc_encoders}")
1455
1516
  else:
1456
- self.logger.warning("⚠️ No NVENC encoders found in FFmpeg")
1517
+ self.logger.debug("No NVENC encoders found in FFmpeg")
1518
+ self.logger.info(" ✗ NVENC not available (no FFmpeg support)")
1457
1519
  return False
1458
1520
  except Exception as e:
1459
- self.logger.warning(f"⚠️ Failed to check FFmpeg NVENC encoders: {e}")
1521
+ self.logger.debug(f"Failed to check FFmpeg NVENC encoders: {e}")
1522
+ self.logger.info(" ✗ NVENC not available")
1460
1523
  return False
1461
1524
 
1462
- # Step 3: Check for libcuda.so.1 (critical for NVENC)
1525
+ # Check for libcuda.so.1 (critical for NVENC)
1463
1526
  try:
1464
1527
  libcuda_check = subprocess.run(["ldconfig", "-p"], capture_output=True, text=True, timeout=10)
1465
1528
  if libcuda_check.returncode == 0 and "libcuda.so.1" in libcuda_check.stdout:
1466
- self.logger.info("libcuda.so.1 found in system libraries")
1529
+ self.logger.debug("libcuda.so.1 found in system libraries")
1467
1530
  else:
1468
- self.logger.warning("libcuda.so.1 NOT found in system libraries")
1469
- self.logger.warning("💡 This usually indicates the CUDA runtime image is needed instead of devel")
1531
+ self.logger.debug("libcuda.so.1 NOT found - may need nvidia/cuda:*-devel image")
1532
+ self.logger.info(" NVENC not available (missing CUDA libraries)")
1470
1533
  return False
1471
1534
  except Exception as e:
1472
- self.logger.warning(f"⚠️ Failed to check for libcuda.so.1: {e}")
1535
+ self.logger.debug(f"Failed to check for libcuda.so.1: {e}")
1536
+ self.logger.info(" ✗ NVENC not available")
1473
1537
  return False
1474
1538
 
1475
- # Step 4: Test h264_nvenc encoder with simple test
1476
- self.logger.info("🧪 Testing h264_nvenc encoder...")
1477
- test_cmd = f"{self.ffmpeg_base_command} -hide_banner -loglevel warning -f lavfi -i testsrc=duration=1:size=320x240:rate=1 -c:v h264_nvenc -f null -"
1478
- self.logger.debug(f"Running test command: {test_cmd}")
1539
+ # Test h264_nvenc encoder
1540
+ test_cmd = f"{self.ffmpeg_base_command} -hide_banner -loglevel error -f lavfi -i testsrc=duration=1:size=320x240:rate=1 -c:v h264_nvenc -f null -"
1541
+ self.logger.debug(f"Testing NVENC: {test_cmd}")
1479
1542
 
1480
1543
  try:
1481
1544
  result = subprocess.run(test_cmd, shell=True, capture_output=True, text=True, timeout=30)
1482
1545
 
1483
1546
  if result.returncode == 0:
1484
- self.logger.info(" NVENC hardware encoding available for video generation")
1485
- self.logger.info(f"Test command succeeded. Output: {result.stderr if result.stderr else '...'}")
1547
+ self.logger.info(" NVENC encoding available")
1486
1548
  return True
1487
1549
  else:
1488
- self.logger.warning(f"NVENC test failed with exit code {result.returncode}")
1489
- if result.stderr:
1490
- self.logger.warning(f"Error output: {result.stderr}")
1491
- if "Cannot load libcuda.so.1" in result.stderr:
1492
- self.logger.warning("💡 Root cause: libcuda.so.1 cannot be loaded by NVENC")
1493
- self.logger.warning("💡 Solution: Use nvidia/cuda:*-devel-* image instead of runtime")
1550
+ self.logger.debug(f"NVENC test failed (exit code {result.returncode}): {result.stderr}")
1551
+ self.logger.info(" ✗ NVENC not available")
1494
1552
  return False
1495
1553
 
1496
1554
  except subprocess.TimeoutExpired:
1497
- self.logger.warning("NVENC test timed out")
1555
+ self.logger.debug("NVENC test timed out")
1556
+ self.logger.info(" ✗ NVENC not available (timeout)")
1498
1557
  return False
1499
1558
 
1500
1559
  except Exception as e:
1501
- self.logger.warning(f"Failed to detect NVENC support: {e}, falling back to software encoding")
1560
+ self.logger.debug(f"Failed to detect NVENC support: {e}")
1561
+ self.logger.info(" ✗ NVENC not available (error)")
1502
1562
  return False
1503
1563
 
1504
1564
  def configure_hardware_acceleration(self):
@@ -1509,12 +1569,12 @@ class KaraokeFinalise:
1509
1569
  # Remove -hwaccel_output_format cuda as it causes pixel format conversion issues
1510
1570
  self.hwaccel_decode_flags = "-hwaccel cuda"
1511
1571
  self.scale_filter = "scale" # Use CPU scaling for complex filter chains
1512
- self.logger.info("Configured for NVIDIA hardware acceleration (simplified for filter compatibility)")
1572
+ self.logger.info("🚀 Using NVENC hardware acceleration for video encoding")
1513
1573
  else:
1514
1574
  self.video_encoder = "libx264"
1515
1575
  self.hwaccel_decode_flags = ""
1516
1576
  self.scale_filter = "scale"
1517
- self.logger.info("Configured for software encoding")
1577
+ self.logger.info("🔧 Using software encoding (libx264) for video")
1518
1578
 
1519
1579
  def get_nvenc_quality_settings(self, quality_mode="high"):
1520
1580
  """Get NVENC settings based on quality requirements."""
@@ -1619,25 +1679,46 @@ class KaraokeFinalise:
1619
1679
  if self.dry_run:
1620
1680
  self.logger.warning("Dry run enabled. No actions will be performed.")
1621
1681
 
1682
+ self.logger.info("=" * 60)
1683
+ self.logger.info("Starting KaraokeFinalise processing pipeline")
1684
+ self.logger.info("=" * 60)
1685
+
1622
1686
  # Check required input files and parameters exist, get user to confirm features before proceeding
1623
1687
  self.validate_input_parameters_for_features()
1624
1688
 
1625
1689
  with_vocals_file = self.find_with_vocals_file()
1626
1690
  base_name, artist, title = self.get_names_from_withvocals(with_vocals_file)
1627
-
1628
- instrumental_audio_file = self.choose_instrumental_audio_file(base_name)
1691
+
1692
+ self.logger.info(f"Processing: {artist} - {title}")
1693
+
1694
+ # Use the selected instrumental file if provided, otherwise search for one
1695
+ if self.selected_instrumental_file:
1696
+ if not os.path.isfile(self.selected_instrumental_file):
1697
+ raise Exception(f"Selected instrumental file not found: {self.selected_instrumental_file}")
1698
+ instrumental_audio_file = self.selected_instrumental_file
1699
+ self.logger.info(f"Using pre-selected instrumental file: {instrumental_audio_file}")
1700
+ else:
1701
+ self.logger.info("No instrumental file pre-selected, searching for instrumental files...")
1702
+ instrumental_audio_file = self.choose_instrumental_audio_file(base_name)
1629
1703
 
1630
1704
  input_files = self.check_input_files_exist(base_name, with_vocals_file, instrumental_audio_file)
1631
1705
  output_files = self.prepare_output_filenames(base_name)
1632
1706
 
1633
1707
  if self.enable_cdg:
1708
+ self.logger.info("Creating CDG package...")
1634
1709
  self.create_cdg_zip_file(input_files, output_files, artist, title)
1710
+ self.logger.info("CDG package created successfully")
1635
1711
 
1636
1712
  if self.enable_txt:
1713
+ self.logger.info("Creating TXT package...")
1637
1714
  self.create_txt_zip_file(input_files, output_files)
1715
+ self.logger.info("TXT package created successfully")
1638
1716
 
1717
+ self.logger.info("Starting video encoding (this is the longest step, ~15-20 minutes)...")
1639
1718
  self.remux_and_encode_output_video_files(with_vocals_file, input_files, output_files)
1719
+ self.logger.info("Video encoding completed successfully")
1640
1720
 
1721
+ self.logger.info("Executing distribution features (YouTube, Dropbox, Discord)...")
1641
1722
  self.execute_optional_features(artist, title, base_name, input_files, output_files, replace_existing)
1642
1723
 
1643
1724
  result = {