karaoke-gen 0.57.0__py3-none-any.whl → 0.71.27__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.
- karaoke_gen/audio_fetcher.py +461 -0
- karaoke_gen/audio_processor.py +407 -30
- karaoke_gen/config.py +62 -113
- karaoke_gen/file_handler.py +32 -59
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +148 -67
- karaoke_gen/karaoke_gen.py +270 -61
- karaoke_gen/lyrics_processor.py +13 -1
- karaoke_gen/metadata.py +78 -73
- karaoke_gen/pipeline/__init__.py +87 -0
- karaoke_gen/pipeline/base.py +215 -0
- karaoke_gen/pipeline/context.py +230 -0
- karaoke_gen/pipeline/executors/__init__.py +21 -0
- karaoke_gen/pipeline/executors/local.py +159 -0
- karaoke_gen/pipeline/executors/remote.py +257 -0
- karaoke_gen/pipeline/stages/__init__.py +27 -0
- karaoke_gen/pipeline/stages/finalize.py +202 -0
- karaoke_gen/pipeline/stages/render.py +165 -0
- karaoke_gen/pipeline/stages/screens.py +139 -0
- karaoke_gen/pipeline/stages/separation.py +191 -0
- karaoke_gen/pipeline/stages/transcription.py +191 -0
- karaoke_gen/style_loader.py +531 -0
- karaoke_gen/utils/bulk_cli.py +6 -0
- karaoke_gen/utils/cli_args.py +424 -0
- karaoke_gen/utils/gen_cli.py +26 -261
- karaoke_gen/utils/remote_cli.py +1965 -0
- karaoke_gen/video_background_processor.py +351 -0
- karaoke_gen-0.71.27.dist-info/METADATA +610 -0
- karaoke_gen-0.71.27.dist-info/RECORD +275 -0
- {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.27.dist-info}/WHEEL +1 -1
- {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.27.dist-info}/entry_points.txt +1 -0
- lyrics_transcriber/__init__.py +10 -0
- lyrics_transcriber/cli/__init__.py +0 -0
- lyrics_transcriber/cli/cli_main.py +285 -0
- lyrics_transcriber/core/__init__.py +0 -0
- lyrics_transcriber/core/config.py +50 -0
- lyrics_transcriber/core/controller.py +520 -0
- lyrics_transcriber/correction/__init__.py +0 -0
- lyrics_transcriber/correction/agentic/__init__.py +9 -0
- lyrics_transcriber/correction/agentic/adapter.py +71 -0
- lyrics_transcriber/correction/agentic/agent.py +313 -0
- lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
- lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
- lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
- lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
- lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
- lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
- lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
- lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
- lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
- lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
- lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
- lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
- lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
- lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
- lyrics_transcriber/correction/agentic/models/enums.py +38 -0
- lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
- lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
- lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
- lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
- lyrics_transcriber/correction/agentic/models/utils.py +19 -0
- lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
- lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
- lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
- lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
- lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
- lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
- lyrics_transcriber/correction/agentic/providers/base.py +36 -0
- lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
- lyrics_transcriber/correction/agentic/providers/config.py +73 -0
- lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
- lyrics_transcriber/correction/agentic/providers/health.py +28 -0
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
- lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
- lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
- lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
- lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
- lyrics_transcriber/correction/agentic/router.py +35 -0
- lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
- lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
- lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
- lyrics_transcriber/correction/anchor_sequence.py +1043 -0
- lyrics_transcriber/correction/corrector.py +760 -0
- lyrics_transcriber/correction/feedback/__init__.py +2 -0
- lyrics_transcriber/correction/feedback/schemas.py +107 -0
- lyrics_transcriber/correction/feedback/store.py +236 -0
- lyrics_transcriber/correction/handlers/__init__.py +0 -0
- lyrics_transcriber/correction/handlers/base.py +52 -0
- lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
- lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
- lyrics_transcriber/correction/handlers/llm.py +293 -0
- lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
- lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
- lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
- lyrics_transcriber/correction/handlers/repeat.py +88 -0
- lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
- lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
- lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
- lyrics_transcriber/correction/handlers/word_operations.py +187 -0
- lyrics_transcriber/correction/operations.py +352 -0
- lyrics_transcriber/correction/phrase_analyzer.py +435 -0
- lyrics_transcriber/correction/text_utils.py +30 -0
- lyrics_transcriber/frontend/.gitignore +23 -0
- lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
- lyrics_transcriber/frontend/.yarnrc.yml +3 -0
- lyrics_transcriber/frontend/README.md +50 -0
- lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
- lyrics_transcriber/frontend/__init__.py +25 -0
- lyrics_transcriber/frontend/eslint.config.js +28 -0
- lyrics_transcriber/frontend/index.html +18 -0
- lyrics_transcriber/frontend/package.json +42 -0
- lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
- lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
- lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
- lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
- lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
- lyrics_transcriber/frontend/public/favicon.ico +0 -0
- lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
- lyrics_transcriber/frontend/src/App.tsx +212 -0
- lyrics_transcriber/frontend/src/api.ts +239 -0
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
- lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
- lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
- lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
- lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
- lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
- lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
- lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
- lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
- lyrics_transcriber/frontend/src/components/Header.tsx +387 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1373 -0
- lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
- lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
- lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +688 -0
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
- lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
- lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
- lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
- lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
- lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
- lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
- lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
- lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
- lyrics_transcriber/frontend/src/main.tsx +17 -0
- lyrics_transcriber/frontend/src/theme.ts +177 -0
- lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
- lyrics_transcriber/frontend/src/types.js +2 -0
- lyrics_transcriber/frontend/src/types.ts +199 -0
- lyrics_transcriber/frontend/src/validation.ts +132 -0
- lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
- lyrics_transcriber/frontend/tsconfig.app.json +26 -0
- lyrics_transcriber/frontend/tsconfig.json +25 -0
- lyrics_transcriber/frontend/tsconfig.node.json +23 -0
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
- lyrics_transcriber/frontend/update_version.js +11 -0
- lyrics_transcriber/frontend/vite.config.d.ts +2 -0
- lyrics_transcriber/frontend/vite.config.js +10 -0
- lyrics_transcriber/frontend/vite.config.ts +11 -0
- lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
- lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
- lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
- lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js +42039 -0
- lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js.map +1 -0
- lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
- lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
- lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
- lyrics_transcriber/frontend/web_assets/index.html +18 -0
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
- lyrics_transcriber/frontend/yarn.lock +3752 -0
- lyrics_transcriber/lyrics/__init__.py +0 -0
- lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
- lyrics_transcriber/lyrics/file_provider.py +95 -0
- lyrics_transcriber/lyrics/genius.py +384 -0
- lyrics_transcriber/lyrics/lrclib.py +231 -0
- lyrics_transcriber/lyrics/musixmatch.py +156 -0
- lyrics_transcriber/lyrics/spotify.py +290 -0
- lyrics_transcriber/lyrics/user_input_provider.py +44 -0
- lyrics_transcriber/output/__init__.py +0 -0
- lyrics_transcriber/output/ass/__init__.py +21 -0
- lyrics_transcriber/output/ass/ass.py +2088 -0
- lyrics_transcriber/output/ass/ass_specs.txt +732 -0
- lyrics_transcriber/output/ass/config.py +180 -0
- lyrics_transcriber/output/ass/constants.py +23 -0
- lyrics_transcriber/output/ass/event.py +94 -0
- lyrics_transcriber/output/ass/formatters.py +132 -0
- lyrics_transcriber/output/ass/lyrics_line.py +265 -0
- lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
- lyrics_transcriber/output/ass/section_detector.py +89 -0
- lyrics_transcriber/output/ass/section_screen.py +106 -0
- lyrics_transcriber/output/ass/style.py +187 -0
- lyrics_transcriber/output/cdg.py +619 -0
- lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
- lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
- lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
- lyrics_transcriber/output/cdgmaker/config.py +151 -0
- lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
- lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
- lyrics_transcriber/output/cdgmaker/pack.py +507 -0
- lyrics_transcriber/output/cdgmaker/render.py +346 -0
- lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
- lyrics_transcriber/output/cdgmaker/utils.py +132 -0
- lyrics_transcriber/output/countdown_processor.py +267 -0
- lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
- lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
- lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/arial.ttf +0 -0
- lyrics_transcriber/output/fonts/georgia.ttf +0 -0
- lyrics_transcriber/output/fonts/verdana.ttf +0 -0
- lyrics_transcriber/output/generator.py +257 -0
- lyrics_transcriber/output/lrc_to_cdg.py +61 -0
- lyrics_transcriber/output/lyrics_file.py +102 -0
- lyrics_transcriber/output/plain_text.py +96 -0
- lyrics_transcriber/output/segment_resizer.py +431 -0
- lyrics_transcriber/output/subtitles.py +397 -0
- lyrics_transcriber/output/video.py +544 -0
- lyrics_transcriber/review/__init__.py +0 -0
- lyrics_transcriber/review/server.py +676 -0
- lyrics_transcriber/storage/__init__.py +0 -0
- lyrics_transcriber/storage/dropbox.py +225 -0
- lyrics_transcriber/transcribers/__init__.py +0 -0
- lyrics_transcriber/transcribers/audioshake.py +290 -0
- lyrics_transcriber/transcribers/base_transcriber.py +157 -0
- lyrics_transcriber/transcribers/whisper.py +330 -0
- lyrics_transcriber/types.py +648 -0
- lyrics_transcriber/utils/__init__.py +0 -0
- lyrics_transcriber/utils/word_utils.py +27 -0
- karaoke_gen-0.57.0.dist-info/METADATA +0 -167
- karaoke_gen-0.57.0.dist-info/RECORD +0 -23
- {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.27.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
|
+
)
|