karaoke-gen 0.75.54__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of karaoke-gen might be problematic. Click here for more details.
- karaoke_gen/__init__.py +38 -0
- karaoke_gen/audio_fetcher.py +1614 -0
- karaoke_gen/audio_processor.py +790 -0
- karaoke_gen/config.py +83 -0
- karaoke_gen/file_handler.py +387 -0
- karaoke_gen/instrumental_review/__init__.py +45 -0
- karaoke_gen/instrumental_review/analyzer.py +408 -0
- karaoke_gen/instrumental_review/editor.py +322 -0
- karaoke_gen/instrumental_review/models.py +171 -0
- karaoke_gen/instrumental_review/server.py +475 -0
- karaoke_gen/instrumental_review/static/index.html +1529 -0
- karaoke_gen/instrumental_review/waveform.py +409 -0
- karaoke_gen/karaoke_finalise/__init__.py +1 -0
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +1833 -0
- karaoke_gen/karaoke_gen.py +1026 -0
- karaoke_gen/lyrics_processor.py +474 -0
- karaoke_gen/metadata.py +160 -0
- 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/resources/AvenirNext-Bold.ttf +0 -0
- karaoke_gen/resources/Montserrat-Bold.ttf +0 -0
- karaoke_gen/resources/Oswald-Bold.ttf +0 -0
- karaoke_gen/resources/Oswald-SemiBold.ttf +0 -0
- karaoke_gen/resources/Zurich_Cn_BT_Bold.ttf +0 -0
- karaoke_gen/style_loader.py +531 -0
- karaoke_gen/utils/__init__.py +18 -0
- karaoke_gen/utils/bulk_cli.py +492 -0
- karaoke_gen/utils/cli_args.py +432 -0
- karaoke_gen/utils/gen_cli.py +978 -0
- karaoke_gen/utils/remote_cli.py +3268 -0
- karaoke_gen/video_background_processor.py +351 -0
- karaoke_gen/video_generator.py +424 -0
- karaoke_gen-0.75.54.dist-info/METADATA +718 -0
- karaoke_gen-0.75.54.dist-info/RECORD +287 -0
- karaoke_gen-0.75.54.dist-info/WHEEL +4 -0
- karaoke_gen-0.75.54.dist-info/entry_points.txt +5 -0
- karaoke_gen-0.75.54.dist-info/licenses/LICENSE +21 -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 +594 -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 +919 -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 +214 -0
- lyrics_transcriber/frontend/src/api.ts +254 -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 +413 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1387 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +185 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +704 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +80 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +905 -0
- lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
- lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +127 -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 +336 -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-BECn1o8Q.js +43288 -0
- lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.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 +306 -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 +379 -0
- lyrics_transcriber/transcribers/base_transcriber.py +157 -0
- lyrics_transcriber/transcribers/whisper.py +330 -0
- lyrics_transcriber/types.py +650 -0
- lyrics_transcriber/utils/__init__.py +0 -0
- lyrics_transcriber/utils/word_utils.py +27 -0
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# Suppress SyntaxWarnings from third-party dependencies (pydub, syrics)
|
|
3
|
+
# that have invalid escape sequences in regex patterns (not yet fixed for Python 3.12+)
|
|
4
|
+
import warnings
|
|
5
|
+
warnings.filterwarnings("ignore", category=SyntaxWarning, module="pydub")
|
|
6
|
+
warnings.filterwarnings("ignore", category=SyntaxWarning, module="syrics")
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import logging
|
|
10
|
+
from importlib import metadata
|
|
11
|
+
import tempfile
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import json
|
|
15
|
+
import asyncio
|
|
16
|
+
import time
|
|
17
|
+
import glob
|
|
18
|
+
import pyperclip
|
|
19
|
+
from karaoke_gen import KaraokePrep
|
|
20
|
+
from karaoke_gen.karaoke_finalise import KaraokeFinalise
|
|
21
|
+
from karaoke_gen.audio_fetcher import UserCancelledError
|
|
22
|
+
from karaoke_gen.instrumental_review import (
|
|
23
|
+
AudioAnalyzer,
|
|
24
|
+
WaveformGenerator,
|
|
25
|
+
InstrumentalReviewServer,
|
|
26
|
+
)
|
|
27
|
+
from .cli_args import create_parser, process_style_overrides, is_url, is_file
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _resolve_path_for_cwd(path: str, track_dir: str) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Resolve a path that may have been created relative to the original working directory.
|
|
33
|
+
|
|
34
|
+
After os.chdir(track_dir), paths like './TrackDir/stems/file.flac' become invalid.
|
|
35
|
+
This function converts such paths to work from the new current directory.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
path: The path to resolve (may be relative or absolute)
|
|
39
|
+
track_dir: The track directory we've chdir'd into
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
A path that's valid from the current working directory
|
|
43
|
+
"""
|
|
44
|
+
if os.path.isabs(path):
|
|
45
|
+
return path
|
|
46
|
+
|
|
47
|
+
# Normalize both paths for comparison
|
|
48
|
+
norm_path = os.path.normpath(path)
|
|
49
|
+
norm_track_dir = os.path.normpath(track_dir)
|
|
50
|
+
|
|
51
|
+
# If path starts with track_dir, strip it to get the relative path from within track_dir
|
|
52
|
+
# e.g., './Four Lanes Male Choir - The White Rose/stems/file.flac' -> 'stems/file.flac'
|
|
53
|
+
if norm_path.startswith(norm_track_dir + os.sep):
|
|
54
|
+
return norm_path[len(norm_track_dir) + 1:]
|
|
55
|
+
elif norm_path.startswith(norm_track_dir):
|
|
56
|
+
return norm_path[len(norm_track_dir):].lstrip(os.sep) or '.'
|
|
57
|
+
|
|
58
|
+
# If path doesn't start with track_dir, it might already be relative to track_dir
|
|
59
|
+
# or it's a path that doesn't need transformation
|
|
60
|
+
return path
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def auto_select_instrumental(track: dict, track_dir: str, logger: logging.Logger) -> str:
|
|
64
|
+
"""
|
|
65
|
+
Auto-select the best instrumental file when --skip_instrumental_review is used.
|
|
66
|
+
|
|
67
|
+
Selection priority:
|
|
68
|
+
1. Padded combined instrumental (+BV) - synchronized with vocals + backing vocals
|
|
69
|
+
2. Non-padded combined instrumental (+BV) - has backing vocals
|
|
70
|
+
3. Padded clean instrumental - synchronized with vocals
|
|
71
|
+
4. Non-padded clean instrumental - basic instrumental
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
track: The track dictionary from KaraokePrep containing separated audio info
|
|
75
|
+
track_dir: The track output directory (we're already chdir'd into it)
|
|
76
|
+
logger: Logger instance
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Path to the selected instrumental file
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
FileNotFoundError: If no suitable instrumental file can be found
|
|
83
|
+
"""
|
|
84
|
+
separated = track.get("separated_audio", {})
|
|
85
|
+
|
|
86
|
+
# Look for combined instrumentals first (they include backing vocals)
|
|
87
|
+
combined = separated.get("combined_instrumentals", {})
|
|
88
|
+
for model, path in combined.items():
|
|
89
|
+
if path:
|
|
90
|
+
resolved = _resolve_path_for_cwd(path, track_dir)
|
|
91
|
+
# Prefer padded version if it exists
|
|
92
|
+
base, ext = os.path.splitext(resolved)
|
|
93
|
+
padded = f"{base} (Padded){ext}"
|
|
94
|
+
if os.path.exists(padded):
|
|
95
|
+
logger.info(f"Auto-selected padded combined instrumental: {padded}")
|
|
96
|
+
return padded
|
|
97
|
+
if os.path.exists(resolved):
|
|
98
|
+
logger.info(f"Auto-selected combined instrumental: {resolved}")
|
|
99
|
+
return resolved
|
|
100
|
+
|
|
101
|
+
# Fall back to clean instrumental
|
|
102
|
+
clean = separated.get("clean_instrumental", {})
|
|
103
|
+
if clean.get("instrumental"):
|
|
104
|
+
resolved = _resolve_path_for_cwd(clean["instrumental"], track_dir)
|
|
105
|
+
# Prefer padded version if it exists
|
|
106
|
+
base, ext = os.path.splitext(resolved)
|
|
107
|
+
padded = f"{base} (Padded){ext}"
|
|
108
|
+
if os.path.exists(padded):
|
|
109
|
+
logger.info(f"Auto-selected padded clean instrumental: {padded}")
|
|
110
|
+
return padded
|
|
111
|
+
if os.path.exists(resolved):
|
|
112
|
+
logger.info(f"Auto-selected clean instrumental: {resolved}")
|
|
113
|
+
return resolved
|
|
114
|
+
|
|
115
|
+
# If separated_audio doesn't have what we need, search the directory
|
|
116
|
+
# This handles edge cases and custom instrumentals
|
|
117
|
+
logger.info("No instrumental found in separated_audio, searching directory...")
|
|
118
|
+
instrumental_files = glob.glob("*(Instrumental*.flac") + glob.glob("*(Instrumental*.wav")
|
|
119
|
+
|
|
120
|
+
# Sort to prefer padded versions and combined instrumentals
|
|
121
|
+
padded_combined = [f for f in instrumental_files if "(Padded)" in f and "+BV" in f]
|
|
122
|
+
if padded_combined:
|
|
123
|
+
logger.info(f"Auto-selected from directory: {padded_combined[0]}")
|
|
124
|
+
return padded_combined[0]
|
|
125
|
+
|
|
126
|
+
padded_files = [f for f in instrumental_files if "(Padded)" in f]
|
|
127
|
+
if padded_files:
|
|
128
|
+
logger.info(f"Auto-selected from directory: {padded_files[0]}")
|
|
129
|
+
return padded_files[0]
|
|
130
|
+
|
|
131
|
+
combined_files = [f for f in instrumental_files if "+BV" in f]
|
|
132
|
+
if combined_files:
|
|
133
|
+
logger.info(f"Auto-selected from directory: {combined_files[0]}")
|
|
134
|
+
return combined_files[0]
|
|
135
|
+
|
|
136
|
+
if instrumental_files:
|
|
137
|
+
logger.info(f"Auto-selected from directory: {instrumental_files[0]}")
|
|
138
|
+
return instrumental_files[0]
|
|
139
|
+
|
|
140
|
+
raise FileNotFoundError(
|
|
141
|
+
"No instrumental file found. Audio separation may have failed. "
|
|
142
|
+
"Check the stems/ directory for separated audio files."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def run_instrumental_review(track: dict, logger: logging.Logger) -> str | None:
|
|
147
|
+
"""
|
|
148
|
+
Run the instrumental review UI to let user select the best instrumental track.
|
|
149
|
+
|
|
150
|
+
This analyzes the backing vocals, generates a waveform, and opens a browser
|
|
151
|
+
with an interactive UI for reviewing and selecting the instrumental.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
track: The track dictionary from KaraokePrep containing separated audio info
|
|
155
|
+
logger: Logger instance
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Path to the selected instrumental file, or None to use the old numeric selection
|
|
159
|
+
"""
|
|
160
|
+
track_dir = track.get("track_output_dir", ".")
|
|
161
|
+
artist = track.get("artist", "")
|
|
162
|
+
title = track.get("title", "")
|
|
163
|
+
base_name = f"{artist} - {title}"
|
|
164
|
+
|
|
165
|
+
# Get separation results
|
|
166
|
+
separated = track.get("separated_audio", {})
|
|
167
|
+
if not separated:
|
|
168
|
+
logger.info("No separated audio found, skipping instrumental review UI")
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
# Find the backing vocals file
|
|
172
|
+
# Note: Paths in separated_audio may be relative to the original working directory,
|
|
173
|
+
# but we've already chdir'd into track_dir. Use _resolve_path_for_cwd to fix paths.
|
|
174
|
+
backing_vocals_path = None
|
|
175
|
+
backing_vocals_result = separated.get("backing_vocals", {})
|
|
176
|
+
for model, paths in backing_vocals_result.items():
|
|
177
|
+
if paths.get("backing_vocals"):
|
|
178
|
+
backing_vocals_path = _resolve_path_for_cwd(paths["backing_vocals"], track_dir)
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
if not backing_vocals_path or not os.path.exists(backing_vocals_path):
|
|
182
|
+
logger.info("No backing vocals file found, skipping instrumental review UI")
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
# Find the clean instrumental file
|
|
186
|
+
clean_result = separated.get("clean_instrumental", {})
|
|
187
|
+
raw_clean_path = clean_result.get("instrumental")
|
|
188
|
+
clean_instrumental_path = _resolve_path_for_cwd(raw_clean_path, track_dir) if raw_clean_path else None
|
|
189
|
+
|
|
190
|
+
if not clean_instrumental_path or not os.path.exists(clean_instrumental_path):
|
|
191
|
+
logger.info("No clean instrumental file found, skipping instrumental review UI")
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
# Find the combined instrumental (with backing vocals) file - these have "(Padded)" suffix if padded
|
|
195
|
+
combined_result = separated.get("combined_instrumentals", {})
|
|
196
|
+
with_backing_path = None
|
|
197
|
+
for model, path in combined_result.items():
|
|
198
|
+
resolved_path = _resolve_path_for_cwd(path, track_dir) if path else None
|
|
199
|
+
if resolved_path and os.path.exists(resolved_path):
|
|
200
|
+
with_backing_path = resolved_path
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
# Find the original audio file (with vocals)
|
|
204
|
+
original_audio_path = None
|
|
205
|
+
raw_original_path = track.get("input_audio_wav")
|
|
206
|
+
if raw_original_path:
|
|
207
|
+
original_audio_path = _resolve_path_for_cwd(raw_original_path, track_dir)
|
|
208
|
+
if not os.path.exists(original_audio_path):
|
|
209
|
+
logger.warning(f"Original audio file not found: {original_audio_path}")
|
|
210
|
+
original_audio_path = None
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
logger.info("=== Starting Instrumental Review ===")
|
|
214
|
+
logger.info(f"Analyzing backing vocals: {backing_vocals_path}")
|
|
215
|
+
|
|
216
|
+
# Analyze backing vocals
|
|
217
|
+
analyzer = AudioAnalyzer()
|
|
218
|
+
analysis = analyzer.analyze(backing_vocals_path)
|
|
219
|
+
|
|
220
|
+
logger.info(f"Analysis complete:")
|
|
221
|
+
logger.info(f" Has audible content: {analysis.has_audible_content}")
|
|
222
|
+
logger.info(f" Total duration: {analysis.total_duration_seconds:.1f}s")
|
|
223
|
+
logger.info(f" Audible segments: {len(analysis.audible_segments)}")
|
|
224
|
+
logger.info(f" Recommendation: {analysis.recommended_selection.value}")
|
|
225
|
+
|
|
226
|
+
# Generate waveform
|
|
227
|
+
# Note: We're already in track_dir after chdir, so use current directory
|
|
228
|
+
logger.info("Generating waveform visualization...")
|
|
229
|
+
waveform_generator = WaveformGenerator()
|
|
230
|
+
waveform_path = f"{base_name} (Backing Vocals Waveform).png"
|
|
231
|
+
waveform_generator.generate(
|
|
232
|
+
audio_path=backing_vocals_path,
|
|
233
|
+
output_path=waveform_path,
|
|
234
|
+
segments=analysis.audible_segments,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Start the review server
|
|
238
|
+
# Note: We're already in track_dir after chdir, so output_dir is "."
|
|
239
|
+
logger.info("Starting instrumental review UI...")
|
|
240
|
+
server = InstrumentalReviewServer(
|
|
241
|
+
output_dir=".",
|
|
242
|
+
base_name=base_name,
|
|
243
|
+
analysis=analysis,
|
|
244
|
+
waveform_path=waveform_path,
|
|
245
|
+
backing_vocals_path=backing_vocals_path,
|
|
246
|
+
clean_instrumental_path=clean_instrumental_path,
|
|
247
|
+
with_backing_path=with_backing_path,
|
|
248
|
+
original_audio_path=original_audio_path,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Start server and open browser, wait for selection
|
|
252
|
+
server.start_and_open_browser()
|
|
253
|
+
|
|
254
|
+
logger.info("Waiting for instrumental selection in browser...")
|
|
255
|
+
logger.info("(Close the browser tab or press Ctrl+C to cancel)")
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
# Wait for user selection (blocking)
|
|
259
|
+
server._selection_event.wait()
|
|
260
|
+
selection = server.get_selection()
|
|
261
|
+
|
|
262
|
+
logger.info(f"User selected: {selection}")
|
|
263
|
+
|
|
264
|
+
# Stop the server
|
|
265
|
+
server.stop()
|
|
266
|
+
|
|
267
|
+
# Return the selected instrumental path
|
|
268
|
+
if selection == "clean":
|
|
269
|
+
return clean_instrumental_path
|
|
270
|
+
elif selection == "with_backing":
|
|
271
|
+
return with_backing_path
|
|
272
|
+
elif selection == "custom":
|
|
273
|
+
custom_path = server.get_custom_instrumental_path()
|
|
274
|
+
if custom_path and os.path.exists(custom_path):
|
|
275
|
+
return custom_path
|
|
276
|
+
else:
|
|
277
|
+
logger.warning("Custom instrumental not found, falling back to clean")
|
|
278
|
+
return clean_instrumental_path
|
|
279
|
+
elif selection == "uploaded":
|
|
280
|
+
uploaded_path = server.get_uploaded_instrumental_path()
|
|
281
|
+
if uploaded_path and os.path.exists(uploaded_path):
|
|
282
|
+
return uploaded_path
|
|
283
|
+
else:
|
|
284
|
+
logger.warning("Uploaded instrumental not found, falling back to clean")
|
|
285
|
+
return clean_instrumental_path
|
|
286
|
+
else:
|
|
287
|
+
logger.warning(f"Unknown selection: {selection}, falling back to numeric selection")
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
except KeyboardInterrupt:
|
|
291
|
+
logger.info("Instrumental review cancelled by user")
|
|
292
|
+
server.stop()
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
except Exception as e:
|
|
296
|
+
logger.error(f"Error during instrumental review: {e}")
|
|
297
|
+
logger.info("Falling back to numeric selection")
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
async def async_main():
|
|
302
|
+
logger = logging.getLogger(__name__)
|
|
303
|
+
# Prevent log propagation to root logger to avoid duplicate logs
|
|
304
|
+
# when external packages (like lyrics_converter) configure root logger handlers
|
|
305
|
+
logger.propagate = False
|
|
306
|
+
log_handler = logging.StreamHandler()
|
|
307
|
+
log_formatter = logging.Formatter(fmt="%(asctime)s.%(msecs)03d - %(levelname)s - %(module)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
|
308
|
+
log_handler.setFormatter(log_formatter)
|
|
309
|
+
logger.addHandler(log_handler)
|
|
310
|
+
|
|
311
|
+
# Use shared CLI parser
|
|
312
|
+
parser = create_parser(prog="karaoke-gen")
|
|
313
|
+
args = parser.parse_args()
|
|
314
|
+
|
|
315
|
+
# Set review UI URL environment variable for the lyrics transcriber review server
|
|
316
|
+
# Only set this if the user explicitly wants to use a dev server (e.g., http://localhost:5173)
|
|
317
|
+
# By default, let the ReviewServer use its bundled local frontend (served from lyrics_transcriber/frontend/)
|
|
318
|
+
# This enables local iteration on the frontend without redeploying
|
|
319
|
+
if hasattr(args, 'review_ui_url') and args.review_ui_url:
|
|
320
|
+
# Check if user provided a custom value (not the default hosted URL)
|
|
321
|
+
default_hosted_urls = [
|
|
322
|
+
'https://gen.nomadkaraoke.com/lyrics',
|
|
323
|
+
'https://lyrics.nomadkaraoke.com'
|
|
324
|
+
]
|
|
325
|
+
if args.review_ui_url.rstrip('/') not in [url.rstrip('/') for url in default_hosted_urls]:
|
|
326
|
+
# User explicitly wants a specific URL (e.g., Vite dev server)
|
|
327
|
+
os.environ['LYRICS_REVIEW_UI_URL'] = args.review_ui_url
|
|
328
|
+
|
|
329
|
+
# Process style overrides
|
|
330
|
+
try:
|
|
331
|
+
style_overrides = process_style_overrides(args.style_override, logger)
|
|
332
|
+
except ValueError:
|
|
333
|
+
sys.exit(1)
|
|
334
|
+
|
|
335
|
+
# Handle test email template case first
|
|
336
|
+
if args.test_email_template:
|
|
337
|
+
log_level = getattr(logging, args.log_level.upper())
|
|
338
|
+
logger.setLevel(log_level)
|
|
339
|
+
logger.info("Testing email template functionality...")
|
|
340
|
+
kfinalise = KaraokeFinalise(
|
|
341
|
+
log_formatter=log_formatter,
|
|
342
|
+
log_level=log_level,
|
|
343
|
+
email_template_file=args.email_template_file,
|
|
344
|
+
)
|
|
345
|
+
kfinalise.test_email_template()
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
# Handle edit-lyrics mode
|
|
349
|
+
if args.edit_lyrics:
|
|
350
|
+
log_level = getattr(logging, args.log_level.upper())
|
|
351
|
+
logger.setLevel(log_level)
|
|
352
|
+
logger.info("Running in edit-lyrics mode...")
|
|
353
|
+
|
|
354
|
+
# Get the current directory name to extract artist and title
|
|
355
|
+
current_dir = os.path.basename(os.getcwd())
|
|
356
|
+
logger.info(f"Current directory: {current_dir}")
|
|
357
|
+
|
|
358
|
+
# Extract artist and title from directory name
|
|
359
|
+
# Format could be either "Artist - Title" or "BRAND-XXXX - Artist - Title"
|
|
360
|
+
if " - " not in current_dir:
|
|
361
|
+
logger.error("Current directory name does not contain ' - ' separator. Cannot extract artist and title.")
|
|
362
|
+
sys.exit(1)
|
|
363
|
+
return # Explicit return for testing
|
|
364
|
+
|
|
365
|
+
parts = current_dir.split(" - ")
|
|
366
|
+
if len(parts) == 2:
|
|
367
|
+
artist, title = parts
|
|
368
|
+
elif len(parts) >= 3:
|
|
369
|
+
# Handle brand code format: "BRAND-XXXX - Artist - Title"
|
|
370
|
+
artist = parts[1]
|
|
371
|
+
title = " - ".join(parts[2:])
|
|
372
|
+
else:
|
|
373
|
+
logger.error(f"Could not parse artist and title from directory name: {current_dir}")
|
|
374
|
+
sys.exit(1)
|
|
375
|
+
return # Explicit return for testing
|
|
376
|
+
|
|
377
|
+
logger.info(f"Extracted artist: {artist}, title: {title}")
|
|
378
|
+
|
|
379
|
+
# Initialize KaraokePrep
|
|
380
|
+
kprep_coroutine = KaraokePrep(
|
|
381
|
+
artist=artist,
|
|
382
|
+
title=title,
|
|
383
|
+
input_media=None, # Will be set by backup_existing_outputs
|
|
384
|
+
dry_run=args.dry_run,
|
|
385
|
+
log_formatter=log_formatter,
|
|
386
|
+
log_level=log_level,
|
|
387
|
+
render_bounding_boxes=args.render_bounding_boxes,
|
|
388
|
+
output_dir=".", # We're already in the track directory
|
|
389
|
+
create_track_subfolders=False, # Don't create subfolders, we're already in one
|
|
390
|
+
lossless_output_format=args.lossless_output_format,
|
|
391
|
+
output_png=args.output_png,
|
|
392
|
+
output_jpg=args.output_jpg,
|
|
393
|
+
clean_instrumental_model=args.clean_instrumental_model,
|
|
394
|
+
backing_vocals_models=args.backing_vocals_models,
|
|
395
|
+
other_stems_models=args.other_stems_models,
|
|
396
|
+
model_file_dir=args.model_file_dir,
|
|
397
|
+
skip_separation=True, # Skip separation as we already have the audio files
|
|
398
|
+
lyrics_artist=args.lyrics_artist or artist,
|
|
399
|
+
lyrics_title=args.lyrics_title or title,
|
|
400
|
+
lyrics_file=args.lyrics_file,
|
|
401
|
+
skip_lyrics=False, # We want to process lyrics
|
|
402
|
+
skip_transcription=False, # We want to transcribe
|
|
403
|
+
skip_transcription_review=args.skip_transcription_review,
|
|
404
|
+
subtitle_offset_ms=args.subtitle_offset_ms,
|
|
405
|
+
style_params_json=args.style_params_json,
|
|
406
|
+
style_overrides=style_overrides,
|
|
407
|
+
background_video=args.background_video,
|
|
408
|
+
background_video_darkness=args.background_video_darkness,
|
|
409
|
+
auto_download=getattr(args, 'auto_download', False),
|
|
410
|
+
)
|
|
411
|
+
# No await needed for constructor
|
|
412
|
+
kprep = kprep_coroutine
|
|
413
|
+
|
|
414
|
+
# Backup existing outputs and get the input audio file
|
|
415
|
+
track_output_dir = os.getcwd()
|
|
416
|
+
input_audio_wav = kprep.file_handler.backup_existing_outputs(track_output_dir, artist, title)
|
|
417
|
+
kprep.input_media = input_audio_wav
|
|
418
|
+
|
|
419
|
+
# Run KaraokePrep
|
|
420
|
+
try:
|
|
421
|
+
tracks = await kprep.process()
|
|
422
|
+
except UserCancelledError:
|
|
423
|
+
logger.info("Operation cancelled by user")
|
|
424
|
+
return
|
|
425
|
+
except KeyboardInterrupt:
|
|
426
|
+
logger.info("Operation cancelled by user (Ctrl+C)")
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
# Filter out None tracks (can happen if prep failed for some tracks)
|
|
430
|
+
tracks = [t for t in tracks if t is not None] if tracks else []
|
|
431
|
+
|
|
432
|
+
if not tracks:
|
|
433
|
+
logger.warning("No tracks to process")
|
|
434
|
+
return
|
|
435
|
+
|
|
436
|
+
# Load CDG styles if CDG generation is enabled
|
|
437
|
+
cdg_styles = None
|
|
438
|
+
if args.enable_cdg:
|
|
439
|
+
if not args.style_params_json:
|
|
440
|
+
logger.error("CDG styles JSON file path (--style_params_json) is required when --enable_cdg is used")
|
|
441
|
+
sys.exit(1)
|
|
442
|
+
return # Explicit return for testing
|
|
443
|
+
try:
|
|
444
|
+
with open(args.style_params_json, "r") as f:
|
|
445
|
+
style_params = json.loads(f.read())
|
|
446
|
+
cdg_styles = style_params["cdg"]
|
|
447
|
+
except FileNotFoundError:
|
|
448
|
+
logger.error(f"CDG styles configuration file not found: {args.style_params_json}")
|
|
449
|
+
sys.exit(1)
|
|
450
|
+
return # Explicit return for testing
|
|
451
|
+
except json.JSONDecodeError as e:
|
|
452
|
+
logger.error(f"Invalid JSON in CDG styles configuration file: {e}")
|
|
453
|
+
sys.exit(1)
|
|
454
|
+
return # Explicit return for testing
|
|
455
|
+
except KeyError:
|
|
456
|
+
logger.error(f"'cdg' key not found in style parameters file: {args.style_params_json}")
|
|
457
|
+
sys.exit(1)
|
|
458
|
+
return # Explicit return for testing
|
|
459
|
+
|
|
460
|
+
# Run KaraokeFinalise with keep_brand_code=True and replace_existing=True
|
|
461
|
+
kfinalise = KaraokeFinalise(
|
|
462
|
+
log_formatter=log_formatter,
|
|
463
|
+
log_level=log_level,
|
|
464
|
+
dry_run=args.dry_run,
|
|
465
|
+
instrumental_format=args.instrumental_format,
|
|
466
|
+
enable_cdg=args.enable_cdg,
|
|
467
|
+
enable_txt=args.enable_txt,
|
|
468
|
+
brand_prefix=args.brand_prefix,
|
|
469
|
+
organised_dir=args.organised_dir,
|
|
470
|
+
organised_dir_rclone_root=args.organised_dir_rclone_root,
|
|
471
|
+
public_share_dir=args.public_share_dir,
|
|
472
|
+
youtube_client_secrets_file=args.youtube_client_secrets_file,
|
|
473
|
+
youtube_description_file=args.youtube_description_file,
|
|
474
|
+
rclone_destination=args.rclone_destination,
|
|
475
|
+
discord_webhook_url=args.discord_webhook_url,
|
|
476
|
+
email_template_file=args.email_template_file,
|
|
477
|
+
cdg_styles=cdg_styles,
|
|
478
|
+
keep_brand_code=True, # Always keep brand code in edit mode
|
|
479
|
+
non_interactive=args.yes,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
try:
|
|
483
|
+
final_track = kfinalise.process(replace_existing=True) # Replace existing YouTube video
|
|
484
|
+
logger.info(f"Successfully completed editing lyrics for: {artist} - {title}")
|
|
485
|
+
|
|
486
|
+
# Display summary of outputs
|
|
487
|
+
logger.info(f"Karaoke lyrics edit complete! Output files:")
|
|
488
|
+
logger.info(f"")
|
|
489
|
+
logger.info(f"Track: {final_track['artist']} - {final_track['title']}")
|
|
490
|
+
logger.info(f"")
|
|
491
|
+
logger.info(f"Working Files:")
|
|
492
|
+
logger.info(f" Video With Vocals: {final_track['video_with_vocals']}")
|
|
493
|
+
logger.info(f" Video With Instrumental: {final_track['video_with_instrumental']}")
|
|
494
|
+
logger.info(f"")
|
|
495
|
+
logger.info(f"Final Videos:")
|
|
496
|
+
logger.info(f" Lossless 4K MP4 (PCM): {final_track['final_video']}")
|
|
497
|
+
logger.info(f" Lossless 4K MKV (FLAC): {final_track['final_video_mkv']}")
|
|
498
|
+
logger.info(f" Lossy 4K MP4 (AAC): {final_track['final_video_lossy']}")
|
|
499
|
+
logger.info(f" Lossy 720p MP4 (AAC): {final_track['final_video_720p']}")
|
|
500
|
+
|
|
501
|
+
if "final_karaoke_cdg_zip" in final_track or "final_karaoke_txt_zip" in final_track:
|
|
502
|
+
logger.info(f"")
|
|
503
|
+
logger.info(f"Karaoke Files:")
|
|
504
|
+
|
|
505
|
+
if "final_karaoke_cdg_zip" in final_track:
|
|
506
|
+
logger.info(f" CDG+MP3 ZIP: {final_track['final_karaoke_cdg_zip']}")
|
|
507
|
+
|
|
508
|
+
if "final_karaoke_txt_zip" in final_track:
|
|
509
|
+
logger.info(f" TXT+MP3 ZIP: {final_track['final_karaoke_txt_zip']}")
|
|
510
|
+
|
|
511
|
+
if final_track["brand_code"]:
|
|
512
|
+
logger.info(f"")
|
|
513
|
+
logger.info(f"Organization:")
|
|
514
|
+
logger.info(f" Brand Code: {final_track['brand_code']}")
|
|
515
|
+
logger.info(f" Directory: {final_track['new_brand_code_dir_path']}")
|
|
516
|
+
|
|
517
|
+
if final_track["youtube_url"] or final_track["brand_code_dir_sharing_link"]:
|
|
518
|
+
logger.info(f"")
|
|
519
|
+
logger.info(f"Sharing:")
|
|
520
|
+
|
|
521
|
+
if final_track["brand_code_dir_sharing_link"]:
|
|
522
|
+
logger.info(f" Folder Link: {final_track['brand_code_dir_sharing_link']}")
|
|
523
|
+
try:
|
|
524
|
+
time.sleep(1) # Brief pause between clipboard operations
|
|
525
|
+
pyperclip.copy(final_track["brand_code_dir_sharing_link"])
|
|
526
|
+
logger.info(f" (Folder link copied to clipboard)")
|
|
527
|
+
except Exception as e:
|
|
528
|
+
logger.warning(f" Failed to copy folder link to clipboard: {str(e)}")
|
|
529
|
+
|
|
530
|
+
if final_track["youtube_url"]:
|
|
531
|
+
logger.info(f" YouTube URL: {final_track['youtube_url']}")
|
|
532
|
+
try:
|
|
533
|
+
pyperclip.copy(final_track["youtube_url"])
|
|
534
|
+
logger.info(f" (YouTube URL copied to clipboard)")
|
|
535
|
+
except Exception as e:
|
|
536
|
+
logger.warning(f" Failed to copy YouTube URL to clipboard: {str(e)}")
|
|
537
|
+
|
|
538
|
+
except Exception as e:
|
|
539
|
+
logger.error(f"Error during finalisation: {str(e)}")
|
|
540
|
+
raise e
|
|
541
|
+
|
|
542
|
+
return
|
|
543
|
+
|
|
544
|
+
# Handle finalise-only mode
|
|
545
|
+
if args.finalise_only:
|
|
546
|
+
log_level = getattr(logging, args.log_level.upper())
|
|
547
|
+
logger.setLevel(log_level)
|
|
548
|
+
logger.info("Running in finalise-only mode...")
|
|
549
|
+
|
|
550
|
+
# Load CDG styles if CDG generation is enabled
|
|
551
|
+
cdg_styles = None
|
|
552
|
+
if args.enable_cdg:
|
|
553
|
+
if not args.style_params_json:
|
|
554
|
+
logger.error("CDG styles JSON file path (--style_params_json) is required when --enable_cdg is used")
|
|
555
|
+
sys.exit(1)
|
|
556
|
+
return # Explicit return for testing
|
|
557
|
+
try:
|
|
558
|
+
with open(args.style_params_json, "r") as f:
|
|
559
|
+
style_params = json.loads(f.read())
|
|
560
|
+
cdg_styles = style_params["cdg"]
|
|
561
|
+
except FileNotFoundError:
|
|
562
|
+
logger.error(f"CDG styles configuration file not found: {args.style_params_json}")
|
|
563
|
+
sys.exit(1)
|
|
564
|
+
return # Explicit return for testing
|
|
565
|
+
except json.JSONDecodeError as e:
|
|
566
|
+
logger.error(f"Invalid JSON in CDG styles configuration file: {e}")
|
|
567
|
+
sys.exit(1)
|
|
568
|
+
return # Explicit return for testing
|
|
569
|
+
except KeyError:
|
|
570
|
+
logger.error(f"'cdg' key not found in style parameters file: {args.style_params_json}")
|
|
571
|
+
sys.exit(1)
|
|
572
|
+
return # Explicit return for testing
|
|
573
|
+
|
|
574
|
+
kfinalise = KaraokeFinalise(
|
|
575
|
+
log_formatter=log_formatter,
|
|
576
|
+
log_level=log_level,
|
|
577
|
+
dry_run=args.dry_run,
|
|
578
|
+
instrumental_format=args.instrumental_format,
|
|
579
|
+
enable_cdg=args.enable_cdg,
|
|
580
|
+
enable_txt=args.enable_txt,
|
|
581
|
+
brand_prefix=args.brand_prefix,
|
|
582
|
+
organised_dir=args.organised_dir,
|
|
583
|
+
organised_dir_rclone_root=args.organised_dir_rclone_root,
|
|
584
|
+
public_share_dir=args.public_share_dir,
|
|
585
|
+
youtube_client_secrets_file=args.youtube_client_secrets_file,
|
|
586
|
+
youtube_description_file=args.youtube_description_file,
|
|
587
|
+
rclone_destination=args.rclone_destination,
|
|
588
|
+
discord_webhook_url=args.discord_webhook_url,
|
|
589
|
+
email_template_file=args.email_template_file,
|
|
590
|
+
cdg_styles=cdg_styles,
|
|
591
|
+
keep_brand_code=getattr(args, 'keep_brand_code', False),
|
|
592
|
+
non_interactive=args.yes,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
try:
|
|
596
|
+
track = kfinalise.process()
|
|
597
|
+
logger.info(f"Successfully completed finalisation for: {track['artist']} - {track['title']}")
|
|
598
|
+
|
|
599
|
+
# Display summary of outputs
|
|
600
|
+
logger.info(f"Karaoke finalisation complete! Output files:")
|
|
601
|
+
logger.info(f"")
|
|
602
|
+
logger.info(f"Track: {track['artist']} - {track['title']}")
|
|
603
|
+
logger.info(f"")
|
|
604
|
+
logger.info(f"Working Files:")
|
|
605
|
+
logger.info(f" Video With Vocals: {track['video_with_vocals']}")
|
|
606
|
+
logger.info(f" Video With Instrumental: {track['video_with_instrumental']}")
|
|
607
|
+
logger.info(f"")
|
|
608
|
+
logger.info(f"Final Videos:")
|
|
609
|
+
logger.info(f" Lossless 4K MP4 (PCM): {track['final_video']}")
|
|
610
|
+
logger.info(f" Lossless 4K MKV (FLAC): {track['final_video_mkv']}")
|
|
611
|
+
logger.info(f" Lossy 4K MP4 (AAC): {track['final_video_lossy']}")
|
|
612
|
+
logger.info(f" Lossy 720p MP4 (AAC): {track['final_video_720p']}")
|
|
613
|
+
|
|
614
|
+
if "final_karaoke_cdg_zip" in track or "final_karaoke_txt_zip" in track:
|
|
615
|
+
logger.info(f"")
|
|
616
|
+
logger.info(f"Karaoke Files:")
|
|
617
|
+
|
|
618
|
+
if "final_karaoke_cdg_zip" in track:
|
|
619
|
+
logger.info(f" CDG+MP3 ZIP: {track['final_karaoke_cdg_zip']}")
|
|
620
|
+
|
|
621
|
+
if "final_karaoke_txt_zip" in track:
|
|
622
|
+
logger.info(f" TXT+MP3 ZIP: {track['final_karaoke_txt_zip']}")
|
|
623
|
+
|
|
624
|
+
if track["brand_code"]:
|
|
625
|
+
logger.info(f"")
|
|
626
|
+
logger.info(f"Organization:")
|
|
627
|
+
logger.info(f" Brand Code: {track['brand_code']}")
|
|
628
|
+
logger.info(f" Directory: {track['new_brand_code_dir_path']}")
|
|
629
|
+
|
|
630
|
+
if track["youtube_url"] or track["brand_code_dir_sharing_link"]:
|
|
631
|
+
logger.info(f"")
|
|
632
|
+
logger.info(f"Sharing:")
|
|
633
|
+
|
|
634
|
+
if track["brand_code_dir_sharing_link"]:
|
|
635
|
+
logger.info(f" Folder Link: {track['brand_code_dir_sharing_link']}")
|
|
636
|
+
try:
|
|
637
|
+
time.sleep(1) # Brief pause between clipboard operations
|
|
638
|
+
pyperclip.copy(track["brand_code_dir_sharing_link"])
|
|
639
|
+
logger.info(f" (Folder link copied to clipboard)")
|
|
640
|
+
except Exception as e:
|
|
641
|
+
logger.warning(f" Failed to copy folder link to clipboard: {str(e)}")
|
|
642
|
+
|
|
643
|
+
if track["youtube_url"]:
|
|
644
|
+
logger.info(f" YouTube URL: {track['youtube_url']}")
|
|
645
|
+
try:
|
|
646
|
+
pyperclip.copy(track["youtube_url"])
|
|
647
|
+
logger.info(f" (YouTube URL copied to clipboard)")
|
|
648
|
+
except Exception as e:
|
|
649
|
+
logger.warning(f" Failed to copy YouTube URL to clipboard: {str(e)}")
|
|
650
|
+
except Exception as e:
|
|
651
|
+
logger.error(f"An error occurred during finalisation, see stack trace below: {str(e)}")
|
|
652
|
+
raise e
|
|
653
|
+
|
|
654
|
+
return
|
|
655
|
+
|
|
656
|
+
# For prep or full workflow, parse input arguments
|
|
657
|
+
input_media, artist, title, filename_pattern = None, None, None, None
|
|
658
|
+
|
|
659
|
+
if not args.args:
|
|
660
|
+
parser.print_help()
|
|
661
|
+
sys.exit(1)
|
|
662
|
+
return # Explicit return for testing
|
|
663
|
+
|
|
664
|
+
# Allow 3 forms of positional arguments:
|
|
665
|
+
# 1. URL or Media File only (may be single track URL, playlist URL, or local file)
|
|
666
|
+
# 2. Artist and Title only
|
|
667
|
+
# 3. URL, Artist, and Title
|
|
668
|
+
if args.args and (is_url(args.args[0]) or is_file(args.args[0])):
|
|
669
|
+
input_media = args.args[0]
|
|
670
|
+
if len(args.args) > 2:
|
|
671
|
+
artist = args.args[1]
|
|
672
|
+
title = args.args[2]
|
|
673
|
+
elif len(args.args) > 1:
|
|
674
|
+
artist = args.args[1]
|
|
675
|
+
else:
|
|
676
|
+
logger.warning("Input media provided without Artist and Title, both will be guessed from title")
|
|
677
|
+
|
|
678
|
+
elif os.path.isdir(args.args[0]):
|
|
679
|
+
if not args.filename_pattern:
|
|
680
|
+
logger.error("Filename pattern is required when processing a folder.")
|
|
681
|
+
sys.exit(1)
|
|
682
|
+
return # Explicit return for testing
|
|
683
|
+
if len(args.args) <= 1:
|
|
684
|
+
logger.error("Second parameter provided must be Artist name; Artist is required when processing a folder.")
|
|
685
|
+
sys.exit(1)
|
|
686
|
+
return # Explicit return for testing
|
|
687
|
+
|
|
688
|
+
input_media = args.args[0]
|
|
689
|
+
artist = args.args[1]
|
|
690
|
+
filename_pattern = args.filename_pattern
|
|
691
|
+
|
|
692
|
+
elif len(args.args) > 1:
|
|
693
|
+
artist = args.args[0]
|
|
694
|
+
title = args.args[1]
|
|
695
|
+
if getattr(args, 'auto_download', False):
|
|
696
|
+
logger.info(f"No input media provided, flacfetch will automatically search and download: {artist} - {title}")
|
|
697
|
+
else:
|
|
698
|
+
logger.info(f"No input media provided, flacfetch will search for: {artist} - {title} (interactive selection)")
|
|
699
|
+
|
|
700
|
+
else:
|
|
701
|
+
parser.print_help()
|
|
702
|
+
sys.exit(1)
|
|
703
|
+
return # Explicit return for testing
|
|
704
|
+
|
|
705
|
+
log_level = getattr(logging, args.log_level.upper())
|
|
706
|
+
logger.setLevel(log_level)
|
|
707
|
+
|
|
708
|
+
# Set up environment variables for lyrics-only mode
|
|
709
|
+
if args.lyrics_only:
|
|
710
|
+
args.skip_separation = True
|
|
711
|
+
os.environ["KARAOKE_GEN_SKIP_AUDIO_SEPARATION"] = "1"
|
|
712
|
+
os.environ["KARAOKE_GEN_SKIP_TITLE_END_SCREENS"] = "1"
|
|
713
|
+
logger.info("Lyrics-only mode enabled: skipping audio separation and title/end screen generation")
|
|
714
|
+
|
|
715
|
+
# Step 1: Run KaraokePrep
|
|
716
|
+
kprep_coroutine = KaraokePrep(
|
|
717
|
+
input_media=input_media,
|
|
718
|
+
artist=artist,
|
|
719
|
+
title=title,
|
|
720
|
+
filename_pattern=filename_pattern,
|
|
721
|
+
dry_run=args.dry_run,
|
|
722
|
+
log_formatter=log_formatter,
|
|
723
|
+
log_level=log_level,
|
|
724
|
+
render_bounding_boxes=args.render_bounding_boxes,
|
|
725
|
+
output_dir=args.output_dir,
|
|
726
|
+
create_track_subfolders=args.no_track_subfolders,
|
|
727
|
+
lossless_output_format=args.lossless_output_format,
|
|
728
|
+
output_png=args.output_png,
|
|
729
|
+
output_jpg=args.output_jpg,
|
|
730
|
+
clean_instrumental_model=args.clean_instrumental_model,
|
|
731
|
+
backing_vocals_models=args.backing_vocals_models,
|
|
732
|
+
other_stems_models=args.other_stems_models,
|
|
733
|
+
model_file_dir=args.model_file_dir,
|
|
734
|
+
existing_instrumental=args.existing_instrumental,
|
|
735
|
+
skip_separation=args.skip_separation,
|
|
736
|
+
lyrics_artist=args.lyrics_artist,
|
|
737
|
+
lyrics_title=args.lyrics_title,
|
|
738
|
+
lyrics_file=args.lyrics_file,
|
|
739
|
+
skip_lyrics=args.skip_lyrics,
|
|
740
|
+
skip_transcription=args.skip_transcription,
|
|
741
|
+
skip_transcription_review=args.skip_transcription_review,
|
|
742
|
+
subtitle_offset_ms=args.subtitle_offset_ms,
|
|
743
|
+
style_params_json=args.style_params_json,
|
|
744
|
+
style_overrides=style_overrides,
|
|
745
|
+
background_video=args.background_video,
|
|
746
|
+
background_video_darkness=args.background_video_darkness,
|
|
747
|
+
auto_download=getattr(args, 'auto_download', False),
|
|
748
|
+
)
|
|
749
|
+
# No await needed for constructor
|
|
750
|
+
kprep = kprep_coroutine
|
|
751
|
+
|
|
752
|
+
# Create final tracks data structure
|
|
753
|
+
try:
|
|
754
|
+
tracks = await kprep.process()
|
|
755
|
+
except UserCancelledError:
|
|
756
|
+
logger.info("Operation cancelled by user")
|
|
757
|
+
return
|
|
758
|
+
except (KeyboardInterrupt, asyncio.CancelledError):
|
|
759
|
+
logger.info("Operation cancelled by user (Ctrl+C)")
|
|
760
|
+
return
|
|
761
|
+
|
|
762
|
+
# Filter out None tracks (can happen if prep failed for some tracks)
|
|
763
|
+
tracks = [t for t in tracks if t is not None] if tracks else []
|
|
764
|
+
|
|
765
|
+
if not tracks:
|
|
766
|
+
logger.warning("No tracks to process")
|
|
767
|
+
return
|
|
768
|
+
|
|
769
|
+
# If prep-only mode, we're done
|
|
770
|
+
if args.prep_only:
|
|
771
|
+
logger.info("Prep-only mode: skipping finalisation phase")
|
|
772
|
+
return
|
|
773
|
+
|
|
774
|
+
# Step 2: For each track, run KaraokeFinalise
|
|
775
|
+
for track in tracks:
|
|
776
|
+
logger.info(f"Starting finalisation phase for {track['artist']} - {track['title']}...")
|
|
777
|
+
|
|
778
|
+
# Use the track directory that was actually created by KaraokePrep
|
|
779
|
+
track_dir = track["track_output_dir"]
|
|
780
|
+
if not os.path.exists(track_dir):
|
|
781
|
+
logger.error(f"Track directory not found: {track_dir}")
|
|
782
|
+
continue
|
|
783
|
+
|
|
784
|
+
logger.info(f"Changing to directory: {track_dir}")
|
|
785
|
+
os.chdir(track_dir)
|
|
786
|
+
|
|
787
|
+
# Select instrumental file - either via web UI, auto-selection, or custom instrumental
|
|
788
|
+
# This ALWAYS produces a selected file - no silent fallback to legacy code
|
|
789
|
+
selected_instrumental_file = None
|
|
790
|
+
skip_review = getattr(args, 'skip_instrumental_review', False)
|
|
791
|
+
|
|
792
|
+
# Check if a custom instrumental was provided (via --existing_instrumental)
|
|
793
|
+
# In this case, the instrumental is already chosen - skip review entirely
|
|
794
|
+
separated_audio = track.get("separated_audio", {})
|
|
795
|
+
custom_instrumental = separated_audio.get("Custom", {}).get("instrumental")
|
|
796
|
+
|
|
797
|
+
if custom_instrumental:
|
|
798
|
+
# Custom instrumental was provided - use it directly, no review needed
|
|
799
|
+
resolved_path = _resolve_path_for_cwd(custom_instrumental, track_dir)
|
|
800
|
+
if os.path.exists(resolved_path):
|
|
801
|
+
logger.info(f"Using custom instrumental (--existing_instrumental): {resolved_path}")
|
|
802
|
+
selected_instrumental_file = resolved_path
|
|
803
|
+
else:
|
|
804
|
+
logger.error(f"Custom instrumental file not found: {resolved_path}")
|
|
805
|
+
logger.error("The file may have been moved or deleted after preparation.")
|
|
806
|
+
sys.exit(1)
|
|
807
|
+
return # Explicit return for testing
|
|
808
|
+
elif skip_review:
|
|
809
|
+
# Auto-select instrumental when review is skipped (non-interactive mode)
|
|
810
|
+
logger.info("Instrumental review skipped (--skip_instrumental_review), auto-selecting instrumental file...")
|
|
811
|
+
try:
|
|
812
|
+
selected_instrumental_file = auto_select_instrumental(
|
|
813
|
+
track=track,
|
|
814
|
+
track_dir=track_dir,
|
|
815
|
+
logger=logger,
|
|
816
|
+
)
|
|
817
|
+
except FileNotFoundError as e:
|
|
818
|
+
logger.error(f"Failed to auto-select instrumental: {e}")
|
|
819
|
+
logger.error("Check that audio separation completed successfully.")
|
|
820
|
+
sys.exit(1)
|
|
821
|
+
return # Explicit return for testing
|
|
822
|
+
else:
|
|
823
|
+
# Run instrumental review web UI
|
|
824
|
+
selected_instrumental_file = run_instrumental_review(
|
|
825
|
+
track=track,
|
|
826
|
+
logger=logger,
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
# If instrumental review failed/returned None, show error and exit
|
|
830
|
+
# NO SILENT FALLBACK - we want to know if the new flow has issues
|
|
831
|
+
if selected_instrumental_file is None:
|
|
832
|
+
logger.error("")
|
|
833
|
+
logger.error("=" * 70)
|
|
834
|
+
logger.error("INSTRUMENTAL SELECTION FAILED")
|
|
835
|
+
logger.error("=" * 70)
|
|
836
|
+
logger.error("")
|
|
837
|
+
logger.error("The instrumental review UI could not find the required files.")
|
|
838
|
+
logger.error("")
|
|
839
|
+
logger.error("Common causes:")
|
|
840
|
+
logger.error(" - No backing vocals file was found (check stems/ directory)")
|
|
841
|
+
logger.error(" - No clean instrumental was found (audio separation may have failed)")
|
|
842
|
+
logger.error(" - Path resolution failed after directory change")
|
|
843
|
+
logger.error("")
|
|
844
|
+
logger.error("To investigate:")
|
|
845
|
+
logger.error(" - Check the stems/ directory for: *Backing Vocals*.flac and *Instrumental*.flac")
|
|
846
|
+
logger.error(" - Look for separation errors earlier in the log")
|
|
847
|
+
logger.error(" - Verify audio separation completed without errors")
|
|
848
|
+
logger.error("")
|
|
849
|
+
logger.error("Workarounds:")
|
|
850
|
+
logger.error(" - Re-run with --skip_instrumental_review to auto-select an instrumental")
|
|
851
|
+
logger.error(" - Re-run the full pipeline to regenerate stems")
|
|
852
|
+
logger.error("")
|
|
853
|
+
sys.exit(1)
|
|
854
|
+
return # Explicit return for testing
|
|
855
|
+
|
|
856
|
+
logger.info(f"Selected instrumental file: {selected_instrumental_file}")
|
|
857
|
+
|
|
858
|
+
# Get countdown padding info from track (if vocals were padded, instrumental must match)
|
|
859
|
+
countdown_padding_seconds = None
|
|
860
|
+
if track.get("countdown_padding_added", False):
|
|
861
|
+
countdown_padding_seconds = track.get("countdown_padding_seconds", 3.0)
|
|
862
|
+
logger.info(f"Countdown padding detected: {countdown_padding_seconds}s (will be applied to instrumental if needed)")
|
|
863
|
+
|
|
864
|
+
# Load CDG styles if CDG generation is enabled
|
|
865
|
+
cdg_styles = None
|
|
866
|
+
if args.enable_cdg:
|
|
867
|
+
if not args.style_params_json:
|
|
868
|
+
logger.error("CDG styles JSON file path (--style_params_json) is required when --enable_cdg is used")
|
|
869
|
+
sys.exit(1)
|
|
870
|
+
return # Explicit return for testing
|
|
871
|
+
try:
|
|
872
|
+
with open(args.style_params_json, "r") as f:
|
|
873
|
+
style_params = json.loads(f.read())
|
|
874
|
+
cdg_styles = style_params["cdg"]
|
|
875
|
+
except FileNotFoundError:
|
|
876
|
+
logger.error(f"CDG styles configuration file not found: {args.style_params_json}")
|
|
877
|
+
sys.exit(1)
|
|
878
|
+
return # Explicit return for testing
|
|
879
|
+
except json.JSONDecodeError as e:
|
|
880
|
+
logger.error(f"Invalid JSON in CDG styles configuration file: {e}")
|
|
881
|
+
sys.exit(1)
|
|
882
|
+
return # Explicit return for testing
|
|
883
|
+
except KeyError:
|
|
884
|
+
logger.error(f"'cdg' key not found in style parameters file: {args.style_params_json}")
|
|
885
|
+
sys.exit(1)
|
|
886
|
+
return # Explicit return for testing
|
|
887
|
+
|
|
888
|
+
kfinalise = KaraokeFinalise(
|
|
889
|
+
log_formatter=log_formatter,
|
|
890
|
+
log_level=log_level,
|
|
891
|
+
dry_run=args.dry_run,
|
|
892
|
+
instrumental_format=args.instrumental_format,
|
|
893
|
+
enable_cdg=args.enable_cdg,
|
|
894
|
+
enable_txt=args.enable_txt,
|
|
895
|
+
brand_prefix=args.brand_prefix,
|
|
896
|
+
organised_dir=args.organised_dir,
|
|
897
|
+
organised_dir_rclone_root=args.organised_dir_rclone_root,
|
|
898
|
+
public_share_dir=args.public_share_dir,
|
|
899
|
+
youtube_client_secrets_file=args.youtube_client_secrets_file,
|
|
900
|
+
youtube_description_file=args.youtube_description_file,
|
|
901
|
+
rclone_destination=args.rclone_destination,
|
|
902
|
+
discord_webhook_url=args.discord_webhook_url,
|
|
903
|
+
email_template_file=args.email_template_file,
|
|
904
|
+
cdg_styles=cdg_styles,
|
|
905
|
+
keep_brand_code=getattr(args, 'keep_brand_code', False),
|
|
906
|
+
non_interactive=args.yes,
|
|
907
|
+
selected_instrumental_file=selected_instrumental_file,
|
|
908
|
+
countdown_padding_seconds=countdown_padding_seconds,
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
try:
|
|
912
|
+
final_track = kfinalise.process()
|
|
913
|
+
logger.info(f"Successfully completed processing: {final_track['artist']} - {final_track['title']}")
|
|
914
|
+
|
|
915
|
+
# Display summary of outputs
|
|
916
|
+
logger.info(f"Karaoke generation complete! Output files:")
|
|
917
|
+
logger.info(f"")
|
|
918
|
+
logger.info(f"Track: {final_track['artist']} - {final_track['title']}")
|
|
919
|
+
logger.info(f"")
|
|
920
|
+
logger.info(f"Working Files:")
|
|
921
|
+
logger.info(f" Video With Vocals: {final_track['video_with_vocals']}")
|
|
922
|
+
logger.info(f" Video With Instrumental: {final_track['video_with_instrumental']}")
|
|
923
|
+
logger.info(f"")
|
|
924
|
+
logger.info(f"Final Videos:")
|
|
925
|
+
logger.info(f" Lossless 4K MP4 (PCM): {final_track['final_video']}")
|
|
926
|
+
logger.info(f" Lossless 4K MKV (FLAC): {final_track['final_video_mkv']}")
|
|
927
|
+
logger.info(f" Lossy 4K MP4 (AAC): {final_track['final_video_lossy']}")
|
|
928
|
+
logger.info(f" Lossy 720p MP4 (AAC): {final_track['final_video_720p']}")
|
|
929
|
+
|
|
930
|
+
if "final_karaoke_cdg_zip" in final_track or "final_karaoke_txt_zip" in final_track:
|
|
931
|
+
logger.info(f"")
|
|
932
|
+
logger.info(f"Karaoke Files:")
|
|
933
|
+
|
|
934
|
+
if "final_karaoke_cdg_zip" in final_track:
|
|
935
|
+
logger.info(f" CDG+MP3 ZIP: {final_track['final_karaoke_cdg_zip']}")
|
|
936
|
+
|
|
937
|
+
if "final_karaoke_txt_zip" in final_track:
|
|
938
|
+
logger.info(f" TXT+MP3 ZIP: {final_track['final_karaoke_txt_zip']}")
|
|
939
|
+
|
|
940
|
+
if final_track["brand_code"]:
|
|
941
|
+
logger.info(f"")
|
|
942
|
+
logger.info(f"Organization:")
|
|
943
|
+
logger.info(f" Brand Code: {final_track['brand_code']}")
|
|
944
|
+
logger.info(f" Directory: {final_track['new_brand_code_dir_path']}")
|
|
945
|
+
|
|
946
|
+
if final_track["youtube_url"] or final_track["brand_code_dir_sharing_link"]:
|
|
947
|
+
logger.info(f"")
|
|
948
|
+
logger.info(f"Sharing:")
|
|
949
|
+
|
|
950
|
+
if final_track["brand_code_dir_sharing_link"]:
|
|
951
|
+
logger.info(f" Folder Link: {final_track['brand_code_dir_sharing_link']}")
|
|
952
|
+
try:
|
|
953
|
+
time.sleep(1) # Brief pause between clipboard operations
|
|
954
|
+
pyperclip.copy(final_track["brand_code_dir_sharing_link"])
|
|
955
|
+
logger.info(f" (Folder link copied to clipboard)")
|
|
956
|
+
except Exception as e:
|
|
957
|
+
logger.warning(f" Failed to copy folder link to clipboard: {str(e)}")
|
|
958
|
+
|
|
959
|
+
if final_track["youtube_url"]:
|
|
960
|
+
logger.info(f" YouTube URL: {final_track['youtube_url']}")
|
|
961
|
+
try:
|
|
962
|
+
pyperclip.copy(final_track["youtube_url"])
|
|
963
|
+
logger.info(f" (YouTube URL copied to clipboard)")
|
|
964
|
+
except Exception as e:
|
|
965
|
+
logger.warning(f" Failed to copy YouTube URL to clipboard: {str(e)}")
|
|
966
|
+
except Exception as e:
|
|
967
|
+
logger.error(f"An error occurred during finalisation, see stack trace below: {str(e)}")
|
|
968
|
+
raise e
|
|
969
|
+
|
|
970
|
+
return
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def main():
|
|
974
|
+
asyncio.run(async_main())
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
if __name__ == "__main__":
|
|
978
|
+
main()
|