karaoke-gen 0.57.0__py3-none-any.whl → 0.71.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. karaoke_gen/audio_fetcher.py +461 -0
  2. karaoke_gen/audio_processor.py +407 -30
  3. karaoke_gen/config.py +62 -113
  4. karaoke_gen/file_handler.py +32 -59
  5. karaoke_gen/karaoke_finalise/karaoke_finalise.py +148 -67
  6. karaoke_gen/karaoke_gen.py +270 -61
  7. karaoke_gen/lyrics_processor.py +13 -1
  8. karaoke_gen/metadata.py +78 -73
  9. karaoke_gen/pipeline/__init__.py +87 -0
  10. karaoke_gen/pipeline/base.py +215 -0
  11. karaoke_gen/pipeline/context.py +230 -0
  12. karaoke_gen/pipeline/executors/__init__.py +21 -0
  13. karaoke_gen/pipeline/executors/local.py +159 -0
  14. karaoke_gen/pipeline/executors/remote.py +257 -0
  15. karaoke_gen/pipeline/stages/__init__.py +27 -0
  16. karaoke_gen/pipeline/stages/finalize.py +202 -0
  17. karaoke_gen/pipeline/stages/render.py +165 -0
  18. karaoke_gen/pipeline/stages/screens.py +139 -0
  19. karaoke_gen/pipeline/stages/separation.py +191 -0
  20. karaoke_gen/pipeline/stages/transcription.py +191 -0
  21. karaoke_gen/style_loader.py +531 -0
  22. karaoke_gen/utils/bulk_cli.py +6 -0
  23. karaoke_gen/utils/cli_args.py +424 -0
  24. karaoke_gen/utils/gen_cli.py +26 -261
  25. karaoke_gen/utils/remote_cli.py +1815 -0
  26. karaoke_gen/video_background_processor.py +351 -0
  27. karaoke_gen-0.71.23.dist-info/METADATA +610 -0
  28. karaoke_gen-0.71.23.dist-info/RECORD +275 -0
  29. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info}/WHEEL +1 -1
  30. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info}/entry_points.txt +1 -0
  31. lyrics_transcriber/__init__.py +10 -0
  32. lyrics_transcriber/cli/__init__.py +0 -0
  33. lyrics_transcriber/cli/cli_main.py +285 -0
  34. lyrics_transcriber/core/__init__.py +0 -0
  35. lyrics_transcriber/core/config.py +50 -0
  36. lyrics_transcriber/core/controller.py +520 -0
  37. lyrics_transcriber/correction/__init__.py +0 -0
  38. lyrics_transcriber/correction/agentic/__init__.py +9 -0
  39. lyrics_transcriber/correction/agentic/adapter.py +71 -0
  40. lyrics_transcriber/correction/agentic/agent.py +313 -0
  41. lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
  42. lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
  43. lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
  44. lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
  45. lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
  46. lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
  47. lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
  48. lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
  49. lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
  50. lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
  51. lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
  52. lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
  53. lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
  54. lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
  55. lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
  56. lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
  57. lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
  58. lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
  59. lyrics_transcriber/correction/agentic/models/enums.py +38 -0
  60. lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
  61. lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
  62. lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
  63. lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
  64. lyrics_transcriber/correction/agentic/models/utils.py +19 -0
  65. lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
  66. lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
  67. lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
  68. lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
  69. lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
  70. lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
  71. lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
  72. lyrics_transcriber/correction/agentic/providers/base.py +36 -0
  73. lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
  74. lyrics_transcriber/correction/agentic/providers/config.py +73 -0
  75. lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
  76. lyrics_transcriber/correction/agentic/providers/health.py +28 -0
  77. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
  78. lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
  79. lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
  80. lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
  81. lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
  82. lyrics_transcriber/correction/agentic/router.py +35 -0
  83. lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
  84. lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
  85. lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
  86. lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
  87. lyrics_transcriber/correction/anchor_sequence.py +1043 -0
  88. lyrics_transcriber/correction/corrector.py +760 -0
  89. lyrics_transcriber/correction/feedback/__init__.py +2 -0
  90. lyrics_transcriber/correction/feedback/schemas.py +107 -0
  91. lyrics_transcriber/correction/feedback/store.py +236 -0
  92. lyrics_transcriber/correction/handlers/__init__.py +0 -0
  93. lyrics_transcriber/correction/handlers/base.py +52 -0
  94. lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
  95. lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
  96. lyrics_transcriber/correction/handlers/llm.py +293 -0
  97. lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
  98. lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
  99. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
  100. lyrics_transcriber/correction/handlers/repeat.py +88 -0
  101. lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
  102. lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
  103. lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
  104. lyrics_transcriber/correction/handlers/word_operations.py +187 -0
  105. lyrics_transcriber/correction/operations.py +352 -0
  106. lyrics_transcriber/correction/phrase_analyzer.py +435 -0
  107. lyrics_transcriber/correction/text_utils.py +30 -0
  108. lyrics_transcriber/frontend/.gitignore +23 -0
  109. lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
  110. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  111. lyrics_transcriber/frontend/README.md +50 -0
  112. lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
  113. lyrics_transcriber/frontend/__init__.py +25 -0
  114. lyrics_transcriber/frontend/eslint.config.js +28 -0
  115. lyrics_transcriber/frontend/index.html +18 -0
  116. lyrics_transcriber/frontend/package.json +42 -0
  117. lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
  118. lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
  119. lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
  120. lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
  121. lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
  122. lyrics_transcriber/frontend/public/favicon.ico +0 -0
  123. lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
  124. lyrics_transcriber/frontend/src/App.tsx +212 -0
  125. lyrics_transcriber/frontend/src/api.ts +239 -0
  126. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
  127. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
  128. lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
  129. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
  130. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
  131. lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
  132. lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
  133. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
  134. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
  135. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
  136. lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
  137. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
  138. lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
  139. lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
  140. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
  141. lyrics_transcriber/frontend/src/components/Header.tsx +387 -0
  142. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1373 -0
  143. lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
  144. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
  145. lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
  146. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
  147. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
  148. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +688 -0
  149. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
  150. lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
  151. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
  152. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
  153. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
  154. lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
  155. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
  156. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
  157. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
  158. lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
  159. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
  160. lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
  161. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  162. lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
  163. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
  164. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  165. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
  166. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
  167. lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
  168. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  169. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
  170. lyrics_transcriber/frontend/src/main.tsx +17 -0
  171. lyrics_transcriber/frontend/src/theme.ts +177 -0
  172. lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
  173. lyrics_transcriber/frontend/src/types.js +2 -0
  174. lyrics_transcriber/frontend/src/types.ts +199 -0
  175. lyrics_transcriber/frontend/src/validation.ts +132 -0
  176. lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
  177. lyrics_transcriber/frontend/tsconfig.app.json +26 -0
  178. lyrics_transcriber/frontend/tsconfig.json +25 -0
  179. lyrics_transcriber/frontend/tsconfig.node.json +23 -0
  180. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
  181. lyrics_transcriber/frontend/update_version.js +11 -0
  182. lyrics_transcriber/frontend/vite.config.d.ts +2 -0
  183. lyrics_transcriber/frontend/vite.config.js +10 -0
  184. lyrics_transcriber/frontend/vite.config.ts +11 -0
  185. lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
  186. lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
  187. lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
  188. lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js +42039 -0
  189. lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js.map +1 -0
  190. lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
  191. lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
  192. lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
  193. lyrics_transcriber/frontend/web_assets/index.html +18 -0
  194. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
  195. lyrics_transcriber/frontend/yarn.lock +3752 -0
  196. lyrics_transcriber/lyrics/__init__.py +0 -0
  197. lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
  198. lyrics_transcriber/lyrics/file_provider.py +95 -0
  199. lyrics_transcriber/lyrics/genius.py +384 -0
  200. lyrics_transcriber/lyrics/lrclib.py +231 -0
  201. lyrics_transcriber/lyrics/musixmatch.py +156 -0
  202. lyrics_transcriber/lyrics/spotify.py +290 -0
  203. lyrics_transcriber/lyrics/user_input_provider.py +44 -0
  204. lyrics_transcriber/output/__init__.py +0 -0
  205. lyrics_transcriber/output/ass/__init__.py +21 -0
  206. lyrics_transcriber/output/ass/ass.py +2088 -0
  207. lyrics_transcriber/output/ass/ass_specs.txt +732 -0
  208. lyrics_transcriber/output/ass/config.py +180 -0
  209. lyrics_transcriber/output/ass/constants.py +23 -0
  210. lyrics_transcriber/output/ass/event.py +94 -0
  211. lyrics_transcriber/output/ass/formatters.py +132 -0
  212. lyrics_transcriber/output/ass/lyrics_line.py +265 -0
  213. lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
  214. lyrics_transcriber/output/ass/section_detector.py +89 -0
  215. lyrics_transcriber/output/ass/section_screen.py +106 -0
  216. lyrics_transcriber/output/ass/style.py +187 -0
  217. lyrics_transcriber/output/cdg.py +619 -0
  218. lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
  219. lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
  220. lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
  221. lyrics_transcriber/output/cdgmaker/config.py +151 -0
  222. lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
  223. lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
  224. lyrics_transcriber/output/cdgmaker/pack.py +507 -0
  225. lyrics_transcriber/output/cdgmaker/render.py +346 -0
  226. lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
  227. lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
  228. lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
  229. lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
  230. lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
  231. lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
  232. lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
  233. lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
  234. lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
  235. lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
  236. lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
  237. lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
  238. lyrics_transcriber/output/cdgmaker/utils.py +132 -0
  239. lyrics_transcriber/output/countdown_processor.py +267 -0
  240. lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
  241. lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
  242. lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
  243. lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
  244. lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
  245. lyrics_transcriber/output/fonts/arial.ttf +0 -0
  246. lyrics_transcriber/output/fonts/georgia.ttf +0 -0
  247. lyrics_transcriber/output/fonts/verdana.ttf +0 -0
  248. lyrics_transcriber/output/generator.py +257 -0
  249. lyrics_transcriber/output/lrc_to_cdg.py +61 -0
  250. lyrics_transcriber/output/lyrics_file.py +102 -0
  251. lyrics_transcriber/output/plain_text.py +96 -0
  252. lyrics_transcriber/output/segment_resizer.py +431 -0
  253. lyrics_transcriber/output/subtitles.py +397 -0
  254. lyrics_transcriber/output/video.py +544 -0
  255. lyrics_transcriber/review/__init__.py +0 -0
  256. lyrics_transcriber/review/server.py +676 -0
  257. lyrics_transcriber/storage/__init__.py +0 -0
  258. lyrics_transcriber/storage/dropbox.py +225 -0
  259. lyrics_transcriber/transcribers/__init__.py +0 -0
  260. lyrics_transcriber/transcribers/audioshake.py +290 -0
  261. lyrics_transcriber/transcribers/base_transcriber.py +157 -0
  262. lyrics_transcriber/transcribers/whisper.py +330 -0
  263. lyrics_transcriber/types.py +648 -0
  264. lyrics_transcriber/utils/__init__.py +0 -0
  265. lyrics_transcriber/utils/word_utils.py +27 -0
  266. karaoke_gen-0.57.0.dist-info/METADATA +0 -167
  267. karaoke_gen-0.57.0.dist-info/RECORD +0 -23
  268. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,159 @@
1
+ """
2
+ Local pipeline executor.
3
+
4
+ This executor runs pipeline stages directly in-process,
5
+ suitable for CLI usage where all processing happens locally.
6
+ """
7
+ import logging
8
+ import time
9
+ from typing import Dict, List
10
+
11
+ from karaoke_gen.pipeline.base import PipelineExecutor, PipelineStage, StageResult, StageStatus
12
+ from karaoke_gen.pipeline.context import PipelineContext
13
+
14
+
15
+ class LocalExecutor(PipelineExecutor):
16
+ """
17
+ Runs pipeline stages directly in-process.
18
+
19
+ This executor is used by the local CLI (karaoke-gen) to run
20
+ all processing stages sequentially on the local machine.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ logger: logging.Logger = None,
26
+ stop_on_failure: bool = True,
27
+ ):
28
+ """
29
+ Initialize the local executor.
30
+
31
+ Args:
32
+ logger: Logger instance
33
+ stop_on_failure: If True, stop pipeline on first failure
34
+ """
35
+ self.logger = logger or logging.getLogger(__name__)
36
+ self.stop_on_failure = stop_on_failure
37
+
38
+ async def run_stage(
39
+ self,
40
+ stage: PipelineStage,
41
+ context: PipelineContext,
42
+ ) -> StageResult:
43
+ """
44
+ Execute a single pipeline stage.
45
+
46
+ Args:
47
+ stage: The stage to execute
48
+ context: Pipeline context
49
+
50
+ Returns:
51
+ Result of stage execution
52
+ """
53
+ self.logger.info(f"Starting stage: {stage.name}")
54
+ context.update_progress(stage.name, 0, f"Starting {stage.name}")
55
+
56
+ # Validate inputs
57
+ if not stage.validate_inputs(context):
58
+ missing = stage.get_missing_inputs(context)
59
+ error_msg = f"Stage {stage.name} missing required inputs: {missing}"
60
+ self.logger.error(error_msg)
61
+ return StageResult(
62
+ status=StageStatus.FAILED,
63
+ error_message=error_msg,
64
+ )
65
+
66
+ start_time = time.time()
67
+
68
+ try:
69
+ # Execute the stage
70
+ result = await stage.execute(context)
71
+
72
+ # Store outputs in context
73
+ if result.success and result.outputs:
74
+ context.set_stage_output(stage.name, result.outputs)
75
+
76
+ # Log result
77
+ duration = time.time() - start_time
78
+ if result.success:
79
+ self.logger.info(f"Stage {stage.name} completed in {duration:.1f}s")
80
+ elif result.status == StageStatus.SKIPPED:
81
+ self.logger.info(f"Stage {stage.name} skipped")
82
+ else:
83
+ self.logger.error(f"Stage {stage.name} failed: {result.error_message}")
84
+
85
+ return result
86
+
87
+ except Exception as e:
88
+ duration = time.time() - start_time
89
+ self.logger.error(f"Stage {stage.name} raised exception: {e}", exc_info=True)
90
+ return StageResult(
91
+ status=StageStatus.FAILED,
92
+ error_message=str(e),
93
+ error_details={"exception_type": type(e).__name__},
94
+ duration_seconds=duration,
95
+ )
96
+ finally:
97
+ # Run cleanup
98
+ try:
99
+ await stage.cleanup(context)
100
+ except Exception as e:
101
+ self.logger.warning(f"Stage {stage.name} cleanup failed: {e}")
102
+
103
+ async def run_pipeline(
104
+ self,
105
+ stages: List[PipelineStage],
106
+ context: PipelineContext,
107
+ ) -> Dict[str, StageResult]:
108
+ """
109
+ Execute a full pipeline of stages.
110
+
111
+ Runs stages sequentially in order, stopping on failure
112
+ if stop_on_failure is True.
113
+
114
+ Args:
115
+ stages: List of stages to execute in order
116
+ context: Pipeline context
117
+
118
+ Returns:
119
+ Dictionary mapping stage names to their results
120
+ """
121
+ results: Dict[str, StageResult] = {}
122
+
123
+ self.logger.info(f"Starting pipeline with {len(stages)} stages")
124
+ pipeline_start = time.time()
125
+
126
+ for i, stage in enumerate(stages):
127
+ self.logger.info(f"Running stage {i+1}/{len(stages)}: {stage.name}")
128
+
129
+ # Calculate overall progress
130
+ base_progress = int((i / len(stages)) * 100)
131
+ context.update_progress(stage.name, base_progress, f"Starting {stage.name}")
132
+
133
+ # Run the stage
134
+ result = await self.run_stage(stage, context)
135
+ results[stage.name] = result
136
+
137
+ # Check for failure
138
+ if result.failed and self.stop_on_failure:
139
+ self.logger.error(f"Pipeline stopped due to failure in stage: {stage.name}")
140
+ break
141
+
142
+ pipeline_duration = time.time() - pipeline_start
143
+
144
+ # Count results
145
+ completed = sum(1 for r in results.values() if r.status == StageStatus.COMPLETED)
146
+ failed = sum(1 for r in results.values() if r.status == StageStatus.FAILED)
147
+ skipped = sum(1 for r in results.values() if r.status == StageStatus.SKIPPED)
148
+
149
+ self.logger.info(
150
+ f"Pipeline completed in {pipeline_duration:.1f}s: "
151
+ f"{completed} completed, {failed} failed, {skipped} skipped"
152
+ )
153
+
154
+ return results
155
+
156
+
157
+ def create_local_executor(logger: logging.Logger = None) -> LocalExecutor:
158
+ """Factory function to create a LocalExecutor."""
159
+ return LocalExecutor(logger=logger)
@@ -0,0 +1,257 @@
1
+ """
2
+ Remote pipeline executor.
3
+
4
+ This executor runs pipeline stages via the backend API,
5
+ suitable for the remote CLI where processing happens in the cloud.
6
+
7
+ Note: This is a placeholder implementation. The actual remote
8
+ execution is handled by the existing backend workers. This executor
9
+ provides a compatible interface for potential future unified
10
+ pipeline execution.
11
+ """
12
+ import logging
13
+ import time
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ from karaoke_gen.pipeline.base import PipelineExecutor, PipelineStage, StageResult, StageStatus
17
+ from karaoke_gen.pipeline.context import PipelineContext
18
+
19
+
20
+ class RemoteExecutor(PipelineExecutor):
21
+ """
22
+ Runs pipeline stages via backend API.
23
+
24
+ This executor is used by the remote CLI (karaoke-gen-remote) to
25
+ submit jobs to the cloud backend and monitor their progress.
26
+
27
+ Note: The current implementation is a compatibility layer.
28
+ The actual processing is handled by the existing backend workers,
29
+ not by executing PipelineStage instances remotely.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ service_url: str,
35
+ auth_token: Optional[str] = None,
36
+ logger: logging.Logger = None,
37
+ poll_interval: int = 5,
38
+ ):
39
+ """
40
+ Initialize the remote executor.
41
+
42
+ Args:
43
+ service_url: Backend service URL
44
+ auth_token: Authentication token
45
+ logger: Logger instance
46
+ poll_interval: Seconds between status polls
47
+ """
48
+ self.service_url = service_url.rstrip('/')
49
+ self.auth_token = auth_token
50
+ self.logger = logger or logging.getLogger(__name__)
51
+ self.poll_interval = poll_interval
52
+ self._session = None
53
+
54
+ @property
55
+ def session(self):
56
+ """Get or create HTTP session."""
57
+ if self._session is None:
58
+ import requests
59
+ self._session = requests.Session()
60
+ if self.auth_token:
61
+ self._session.headers['Authorization'] = f'Bearer {self.auth_token}'
62
+ return self._session
63
+
64
+ async def run_stage(
65
+ self,
66
+ stage: PipelineStage,
67
+ context: PipelineContext,
68
+ ) -> StageResult:
69
+ """
70
+ Execute a single pipeline stage via backend.
71
+
72
+ Note: This is a placeholder. The backend handles stage
73
+ execution internally through its worker system.
74
+
75
+ Args:
76
+ stage: The stage to execute
77
+ context: Pipeline context
78
+
79
+ Returns:
80
+ Result of stage execution
81
+ """
82
+ self.logger.warning(
83
+ f"RemoteExecutor.run_stage called for {stage.name}. "
84
+ "Remote execution is handled by backend workers, not via this interface."
85
+ )
86
+
87
+ return StageResult(
88
+ status=StageStatus.SKIPPED,
89
+ error_message="Remote execution handled by backend workers",
90
+ )
91
+
92
+ async def run_pipeline(
93
+ self,
94
+ stages: List[PipelineStage],
95
+ context: PipelineContext,
96
+ ) -> Dict[str, StageResult]:
97
+ """
98
+ Execute a full pipeline via backend.
99
+
100
+ This submits a job to the backend and monitors its progress.
101
+ The backend handles individual stage execution through its
102
+ worker system.
103
+
104
+ Args:
105
+ stages: List of stages (used for validation only)
106
+ context: Pipeline context with job parameters
107
+
108
+ Returns:
109
+ Dictionary mapping stage names to their results
110
+ """
111
+ results: Dict[str, StageResult] = {}
112
+
113
+ try:
114
+ # Submit job to backend
115
+ job_id = await self._submit_job(context)
116
+ context.log("INFO", f"Job submitted: {job_id}")
117
+
118
+ # Monitor job progress
119
+ final_status = await self._monitor_job(job_id, context)
120
+
121
+ # Build results based on final status
122
+ if final_status == "complete":
123
+ # Mark all stages as completed
124
+ for stage in stages:
125
+ results[stage.name] = StageResult(status=StageStatus.COMPLETED)
126
+ else:
127
+ # Mark stages based on where failure occurred
128
+ for stage in stages:
129
+ results[stage.name] = StageResult(
130
+ status=StageStatus.FAILED,
131
+ error_message=f"Job ended with status: {final_status}",
132
+ )
133
+
134
+ return results
135
+
136
+ except Exception as e:
137
+ self.logger.error(f"Remote pipeline execution failed: {e}")
138
+ for stage in stages:
139
+ results[stage.name] = StageResult(
140
+ status=StageStatus.FAILED,
141
+ error_message=str(e),
142
+ )
143
+ return results
144
+
145
+ async def _submit_job(self, context: PipelineContext) -> str:
146
+ """
147
+ Submit a job to the backend.
148
+
149
+ Args:
150
+ context: Pipeline context with job parameters
151
+
152
+ Returns:
153
+ Job ID
154
+ """
155
+ import os
156
+
157
+ # Build form data
158
+ data = {
159
+ 'artist': context.artist,
160
+ 'title': context.title,
161
+ 'enable_cdg': str(context.enable_cdg).lower(),
162
+ 'enable_txt': str(context.enable_txt).lower(),
163
+ }
164
+
165
+ if context.brand_prefix:
166
+ data['brand_prefix'] = context.brand_prefix
167
+ if context.discord_webhook_url:
168
+ data['discord_webhook_url'] = context.discord_webhook_url
169
+ if context.enable_youtube_upload:
170
+ data['enable_youtube_upload'] = str(context.enable_youtube_upload).lower()
171
+ if context.dropbox_path:
172
+ data['dropbox_path'] = context.dropbox_path
173
+ if context.gdrive_folder_id:
174
+ data['gdrive_folder_id'] = context.gdrive_folder_id
175
+
176
+ # Upload audio file
177
+ files = {}
178
+ if os.path.isfile(context.input_audio_path):
179
+ files['file'] = (
180
+ os.path.basename(context.input_audio_path),
181
+ open(context.input_audio_path, 'rb'),
182
+ )
183
+
184
+ try:
185
+ response = self.session.post(
186
+ f"{self.service_url}/api/jobs/upload",
187
+ data=data,
188
+ files=files,
189
+ )
190
+ response.raise_for_status()
191
+ result = response.json()
192
+ return result['job_id']
193
+ finally:
194
+ # Close file handles
195
+ for name, (filename, fh) in files.items():
196
+ fh.close()
197
+
198
+ async def _monitor_job(self, job_id: str, context: PipelineContext) -> str:
199
+ """
200
+ Monitor job progress until completion.
201
+
202
+ Args:
203
+ job_id: Job ID to monitor
204
+ context: Pipeline context for progress updates
205
+
206
+ Returns:
207
+ Final job status
208
+ """
209
+ import asyncio
210
+
211
+ while True:
212
+ try:
213
+ response = self.session.get(f"{self.service_url}/api/jobs/{job_id}")
214
+ response.raise_for_status()
215
+ job_data = response.json()
216
+
217
+ status = job_data.get('status', 'unknown')
218
+ progress = job_data.get('progress', 0)
219
+
220
+ context.update_progress(status, progress, f"Status: {status}")
221
+
222
+ # Check for terminal states
223
+ if status in ['complete', 'failed', 'cancelled', 'error']:
224
+ return status
225
+
226
+ await asyncio.sleep(self.poll_interval)
227
+
228
+ except Exception as e:
229
+ self.logger.warning(f"Error polling job status: {e}")
230
+ await asyncio.sleep(self.poll_interval)
231
+
232
+ def get_job_status(self, job_id: str) -> Dict[str, Any]:
233
+ """
234
+ Get current job status.
235
+
236
+ Args:
237
+ job_id: Job ID
238
+
239
+ Returns:
240
+ Job status data
241
+ """
242
+ response = self.session.get(f"{self.service_url}/api/jobs/{job_id}")
243
+ response.raise_for_status()
244
+ return response.json()
245
+
246
+
247
+ def create_remote_executor(
248
+ service_url: str,
249
+ auth_token: Optional[str] = None,
250
+ logger: logging.Logger = None,
251
+ ) -> RemoteExecutor:
252
+ """Factory function to create a RemoteExecutor."""
253
+ return RemoteExecutor(
254
+ service_url=service_url,
255
+ auth_token=auth_token,
256
+ logger=logger,
257
+ )
@@ -0,0 +1,27 @@
1
+ """
2
+ Pipeline stages for karaoke generation.
3
+
4
+ Each stage represents a discrete unit of work in the karaoke generation
5
+ process. Stages are designed to be composable and can be executed either
6
+ locally or remotely.
7
+
8
+ Available stages:
9
+ - SeparationStage: Audio separation into stems
10
+ - TranscriptionStage: Lyrics transcription and synchronization
11
+ - ScreensStage: Title and end screen generation
12
+ - RenderStage: Video rendering with synchronized lyrics
13
+ - FinalizeStage: Encoding, packaging, and distribution
14
+ """
15
+ from karaoke_gen.pipeline.stages.separation import SeparationStage
16
+ from karaoke_gen.pipeline.stages.transcription import TranscriptionStage
17
+ from karaoke_gen.pipeline.stages.screens import ScreensStage
18
+ from karaoke_gen.pipeline.stages.render import RenderStage
19
+ from karaoke_gen.pipeline.stages.finalize import FinalizeStage
20
+
21
+ __all__ = [
22
+ "SeparationStage",
23
+ "TranscriptionStage",
24
+ "ScreensStage",
25
+ "RenderStage",
26
+ "FinalizeStage",
27
+ ]
@@ -0,0 +1,202 @@
1
+ """
2
+ Finalization pipeline stage.
3
+
4
+ This stage handles the final processing:
5
+ - Multi-format video encoding (4K lossless, 4K lossy, 720p)
6
+ - CDG/TXT package generation
7
+ - Distribution (YouTube, Dropbox, Google Drive)
8
+ - Discord notifications
9
+
10
+ This wraps the existing KaraokeFinalise module.
11
+ """
12
+ import logging
13
+ import os
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ from karaoke_gen.pipeline.base import PipelineStage, StageResult, StageStatus
17
+ from karaoke_gen.pipeline.context import PipelineContext
18
+
19
+
20
+ class FinalizeStage(PipelineStage):
21
+ """
22
+ Finalization stage.
23
+
24
+ Handles encoding, packaging, and distribution of the final
25
+ karaoke video and related files.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ enable_cdg: bool = True,
31
+ enable_txt: bool = True,
32
+ non_interactive: bool = False,
33
+ server_side_mode: bool = False,
34
+ youtube_credentials: Optional[Dict[str, Any]] = None,
35
+ youtube_description_template: Optional[str] = None,
36
+ logger: Optional[logging.Logger] = None,
37
+ ):
38
+ """
39
+ Initialize the finalize stage.
40
+
41
+ Args:
42
+ enable_cdg: Generate CDG+MP3 package
43
+ enable_txt: Generate TXT+MP3 package
44
+ non_interactive: Run without user prompts
45
+ server_side_mode: Running on server (disables local-only features)
46
+ youtube_credentials: Pre-loaded YouTube OAuth credentials
47
+ youtube_description_template: YouTube video description template
48
+ logger: Logger instance
49
+ """
50
+ self.enable_cdg = enable_cdg
51
+ self.enable_txt = enable_txt
52
+ self.non_interactive = non_interactive
53
+ self.server_side_mode = server_side_mode
54
+ self.youtube_credentials = youtube_credentials
55
+ self.youtube_description_template = youtube_description_template
56
+ self.logger = logger or logging.getLogger(__name__)
57
+
58
+ @property
59
+ def name(self) -> str:
60
+ return "finalize"
61
+
62
+ @property
63
+ def required_inputs(self) -> List[str]:
64
+ # Requires render output (with_vocals video)
65
+ return ["render", "screens"]
66
+
67
+ @property
68
+ def optional_inputs(self) -> List[str]:
69
+ return ["separation"]
70
+
71
+ @property
72
+ def output_keys(self) -> List[str]:
73
+ return [
74
+ "final_video_lossless_4k_mp4",
75
+ "final_video_lossless_4k_mkv",
76
+ "final_video_lossy_4k_mp4",
77
+ "final_video_lossy_720p_mp4",
78
+ "cdg_zip_path",
79
+ "txt_zip_path",
80
+ "brand_code",
81
+ "youtube_url",
82
+ "dropbox_link",
83
+ ]
84
+
85
+ async def execute(self, context: PipelineContext) -> StageResult:
86
+ """
87
+ Execute finalization.
88
+
89
+ Args:
90
+ context: Pipeline context with render outputs
91
+
92
+ Returns:
93
+ StageResult with final file paths
94
+ """
95
+ import time
96
+ start_time = time.time()
97
+
98
+ try:
99
+ context.update_progress(self.name, 0, "Starting finalization")
100
+ context.log("INFO", f"Finalizing: {context.artist} - {context.title}")
101
+
102
+ from karaoke_gen.karaoke_finalise.karaoke_finalise import KaraokeFinalise
103
+
104
+ # Get the selected instrumental path
105
+ separation = context.stage_outputs.get("separation", {})
106
+
107
+ # Prefer combined instrumental with backing if available
108
+ instrumental_path = None
109
+ combined = separation.get("combined_instrumentals", {})
110
+ if combined:
111
+ # Get first combined instrumental
112
+ instrumental_path = list(combined.values())[0]
113
+ elif separation.get("clean_instrumental", {}).get("instrumental"):
114
+ instrumental_path = separation["clean_instrumental"]["instrumental"]
115
+
116
+ if not instrumental_path or not os.path.exists(instrumental_path):
117
+ return StageResult(
118
+ status=StageStatus.FAILED,
119
+ error_message="No instrumental audio file found",
120
+ )
121
+
122
+ # Build CDG styles from context style_params
123
+ cdg_styles = None
124
+ if context.style_params and context.style_params.get("cdg"):
125
+ cdg_styles = context.style_params["cdg"]
126
+
127
+ context.update_progress(self.name, 10, "Initializing KaraokeFinalise")
128
+
129
+ # Create KaraokeFinalise instance
130
+ finalise = KaraokeFinalise(
131
+ logger=self.logger,
132
+ log_level=logging.INFO,
133
+ dry_run=False,
134
+ instrumental_format="flac",
135
+ enable_cdg=self.enable_cdg or context.enable_cdg,
136
+ enable_txt=self.enable_txt or context.enable_txt,
137
+ cdg_styles=cdg_styles,
138
+ brand_prefix=context.brand_prefix,
139
+ organised_dir=None, # Not used directly
140
+ organised_dir_rclone_root=context.organised_dir_rclone_root,
141
+ public_share_dir=None,
142
+ discord_webhook_url=context.discord_webhook_url,
143
+ youtube_client_secrets_file=None,
144
+ youtube_description_file=None, # Use template instead
145
+ user_youtube_credentials=self.youtube_credentials,
146
+ rclone_destination=context.rclone_destination,
147
+ email_template_file=None,
148
+ non_interactive=self.non_interactive,
149
+ server_side_mode=self.server_side_mode,
150
+ selected_instrumental_file=instrumental_path,
151
+ )
152
+
153
+ # Change to output directory for KaraokeFinalise
154
+ original_cwd = os.getcwd()
155
+ try:
156
+ os.chdir(context.output_dir)
157
+
158
+ context.update_progress(self.name, 20, "Processing finalization")
159
+
160
+ # Run finalization
161
+ result = finalise.process()
162
+
163
+ finally:
164
+ os.chdir(original_cwd)
165
+
166
+ # Build outputs from result
167
+ outputs = {
168
+ "final_video_lossless_4k_mp4": result.get("final_video"),
169
+ "final_video_lossless_4k_mkv": result.get("final_video_mkv"),
170
+ "final_video_lossy_4k_mp4": result.get("final_video_lossy"),
171
+ "final_video_lossy_720p_mp4": result.get("final_video_720p"),
172
+ "cdg_zip_path": result.get("final_karaoke_cdg_zip"),
173
+ "txt_zip_path": result.get("final_karaoke_txt_zip"),
174
+ "brand_code": result.get("brand_code"),
175
+ "youtube_url": result.get("youtube_url"),
176
+ }
177
+
178
+ context.update_progress(self.name, 100, "Finalization complete")
179
+
180
+ duration = time.time() - start_time
181
+ context.log("INFO", f"Finalization completed in {duration:.1f}s")
182
+
183
+ if outputs.get("brand_code"):
184
+ context.log("INFO", f"Brand code: {outputs['brand_code']}")
185
+ if outputs.get("youtube_url"):
186
+ context.log("INFO", f"YouTube URL: {outputs['youtube_url']}")
187
+
188
+ return StageResult(
189
+ status=StageStatus.COMPLETED,
190
+ outputs=outputs,
191
+ duration_seconds=duration,
192
+ )
193
+
194
+ except Exception as e:
195
+ duration = time.time() - start_time
196
+ context.log("ERROR", f"Finalization failed: {str(e)}")
197
+ return StageResult(
198
+ status=StageStatus.FAILED,
199
+ error_message=str(e),
200
+ error_details={"exception_type": type(e).__name__},
201
+ duration_seconds=duration,
202
+ )