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
karaoke_gen/karaoke_gen.py
CHANGED
|
@@ -28,6 +28,8 @@ from .file_handler import FileHandler
|
|
|
28
28
|
from .audio_processor import AudioProcessor
|
|
29
29
|
from .lyrics_processor import LyricsProcessor
|
|
30
30
|
from .video_generator import VideoGenerator
|
|
31
|
+
from .video_background_processor import VideoBackgroundProcessor
|
|
32
|
+
from .audio_fetcher import create_audio_fetcher, AudioFetcherError, NoResultsError
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
class KaraokePrep:
|
|
@@ -67,10 +69,14 @@ class KaraokePrep:
|
|
|
67
69
|
subtitle_offset_ms=0,
|
|
68
70
|
# Style Configuration
|
|
69
71
|
style_params_json=None,
|
|
72
|
+
style_overrides=None,
|
|
70
73
|
# Add the new parameter
|
|
71
74
|
skip_separation=False,
|
|
72
|
-
#
|
|
73
|
-
|
|
75
|
+
# Video Background Configuration
|
|
76
|
+
background_video=None,
|
|
77
|
+
background_video_darkness=0,
|
|
78
|
+
# Audio Fetcher Configuration
|
|
79
|
+
auto_download=False,
|
|
74
80
|
):
|
|
75
81
|
self.log_level = log_level
|
|
76
82
|
self.log_formatter = log_formatter
|
|
@@ -124,13 +130,30 @@ class KaraokePrep:
|
|
|
124
130
|
|
|
125
131
|
# Style Config - Keep needed ones
|
|
126
132
|
self.render_bounding_boxes = render_bounding_boxes # Passed to VideoGenerator
|
|
127
|
-
self.style_params_json = style_params_json
|
|
133
|
+
self.style_params_json = style_params_json
|
|
134
|
+
self.style_overrides = style_overrides
|
|
135
|
+
self.temp_style_file = None
|
|
128
136
|
|
|
129
|
-
#
|
|
130
|
-
self.
|
|
137
|
+
# Video Background Config
|
|
138
|
+
self.background_video = background_video
|
|
139
|
+
self.background_video_darkness = background_video_darkness
|
|
140
|
+
|
|
141
|
+
# Audio Fetcher Config (replaces yt-dlp)
|
|
142
|
+
self.auto_download = auto_download # If True, automatically select best audio source
|
|
143
|
+
|
|
144
|
+
# Initialize audio fetcher for searching and downloading audio when no input file is provided
|
|
145
|
+
self.audio_fetcher = create_audio_fetcher(logger=self.logger)
|
|
131
146
|
|
|
132
147
|
# Load style parameters using the config module
|
|
133
|
-
self.style_params = load_style_params(self.style_params_json, self.logger)
|
|
148
|
+
self.style_params = load_style_params(self.style_params_json, self.style_overrides, self.logger)
|
|
149
|
+
|
|
150
|
+
# If overrides were applied, write to a temp file and update the path
|
|
151
|
+
if self.style_overrides:
|
|
152
|
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".json") as temp_file:
|
|
153
|
+
json.dump(self.style_params, temp_file, indent=2)
|
|
154
|
+
self.temp_style_file = temp_file.name
|
|
155
|
+
self.style_params_json = self.temp_style_file
|
|
156
|
+
self.logger.info(f"Style overrides applied. Using temporary style file: {self.temp_style_file}")
|
|
134
157
|
|
|
135
158
|
# Set up title and end formats using the config module
|
|
136
159
|
self.title_format = setup_title_format(self.style_params)
|
|
@@ -181,6 +204,16 @@ class KaraokePrep:
|
|
|
181
204
|
output_jpg=self.output_jpg,
|
|
182
205
|
)
|
|
183
206
|
|
|
207
|
+
# Instantiate VideoBackgroundProcessor if background_video is provided
|
|
208
|
+
if self.background_video:
|
|
209
|
+
self.logger.info(f"Video background enabled: {self.background_video}")
|
|
210
|
+
self.video_background_processor = VideoBackgroundProcessor(
|
|
211
|
+
logger=self.logger,
|
|
212
|
+
ffmpeg_base_command=self.ffmpeg_base_command,
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
self.video_background_processor = None
|
|
216
|
+
|
|
184
217
|
self.logger.debug(f"Initialized title_format with extra_text: {self.title_format['extra_text']}")
|
|
185
218
|
self.logger.debug(f"Initialized title_format with extra_text_region: {self.title_format['extra_text_region']}")
|
|
186
219
|
|
|
@@ -199,10 +232,19 @@ class KaraokePrep:
|
|
|
199
232
|
else:
|
|
200
233
|
self.logger.debug(f"Overall output dir {self.output_dir} already exists")
|
|
201
234
|
|
|
235
|
+
def __del__(self):
|
|
236
|
+
# Cleanup the temporary style file if it was created
|
|
237
|
+
if self.temp_style_file and os.path.exists(self.temp_style_file):
|
|
238
|
+
try:
|
|
239
|
+
os.remove(self.temp_style_file)
|
|
240
|
+
self.logger.debug(f"Removed temporary style file: {self.temp_style_file}")
|
|
241
|
+
except OSError as e:
|
|
242
|
+
self.logger.warning(f"Error removing temporary style file {self.temp_style_file}: {e}")
|
|
243
|
+
|
|
202
244
|
# Compatibility methods for tests - these call the new functions in metadata.py
|
|
203
245
|
def extract_info_for_online_media(self, input_url=None, input_artist=None, input_title=None):
|
|
204
246
|
"""Compatibility method that calls the function in metadata.py"""
|
|
205
|
-
self.extracted_info = extract_info_for_online_media(input_url, input_artist, input_title, self.logger
|
|
247
|
+
self.extracted_info = extract_info_for_online_media(input_url, input_artist, input_title, self.logger)
|
|
206
248
|
return self.extracted_info
|
|
207
249
|
|
|
208
250
|
def parse_single_track_metadata(self, input_artist, input_title):
|
|
@@ -327,51 +369,77 @@ class KaraokePrep:
|
|
|
327
369
|
processed_track["input_audio_wav"] = self.file_handler.convert_to_wav(processed_track["input_media"], output_filename_no_extension)
|
|
328
370
|
|
|
329
371
|
else:
|
|
330
|
-
# ---
|
|
372
|
+
# --- AudioFetcher or Existing Files Handling ---
|
|
331
373
|
# Construct patterns using the determined extractor
|
|
332
374
|
base_pattern = os.path.join(track_output_dir, f"{artist_title} ({self.extractor}*)")
|
|
333
|
-
input_media_glob = glob.glob(f"{base_pattern}.*
|
|
375
|
+
input_media_glob = glob.glob(f"{base_pattern}.*flac") + glob.glob(f"{base_pattern}.*mp3") + glob.glob(f"{base_pattern}.*wav") + glob.glob(f"{base_pattern}.*webm") + glob.glob(f"{base_pattern}.*mp4")
|
|
334
376
|
input_png_glob = glob.glob(f"{base_pattern}.png")
|
|
335
377
|
input_wav_glob = glob.glob(f"{base_pattern}.wav")
|
|
336
378
|
|
|
337
|
-
if input_media_glob and
|
|
379
|
+
if input_media_glob and input_wav_glob:
|
|
338
380
|
# Existing files found
|
|
339
381
|
processed_track["input_media"] = input_media_glob[0]
|
|
340
|
-
processed_track["input_still_image"] = input_png_glob[0]
|
|
382
|
+
processed_track["input_still_image"] = input_png_glob[0] if input_png_glob else None
|
|
341
383
|
processed_track["input_audio_wav"] = input_wav_glob[0]
|
|
342
384
|
self.logger.info(f"Found existing media files matching extractor '{self.extractor}', skipping download/conversion.")
|
|
343
|
-
# Ensure self.extractor reflects the found files if it was a fallback
|
|
344
|
-
# Extract the actual extractor string from the filename if needed, though it should match
|
|
345
|
-
|
|
346
|
-
elif self.url: # URL provided and files not found, proceed with download
|
|
347
|
-
# Use media_id if available for better uniqueness
|
|
348
|
-
filename_suffix = f"{self.extractor} {self.media_id}" if self.media_id else self.extractor
|
|
349
|
-
output_filename_no_extension = os.path.join(track_output_dir, f"{artist_title} ({filename_suffix})")
|
|
350
|
-
|
|
351
|
-
self.logger.info(f"Downloading input media from {self.url}...")
|
|
352
|
-
# Delegate to FileHandler
|
|
353
|
-
processed_track["input_media"] = self.file_handler.download_video(self.url, output_filename_no_extension, self.cookies_str)
|
|
354
385
|
|
|
355
|
-
|
|
356
|
-
#
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
386
|
+
elif getattr(self, '_use_audio_fetcher', False):
|
|
387
|
+
# Use flacfetch to search and download audio
|
|
388
|
+
self.logger.info(f"Using flacfetch to search and download: {self.artist} - {self.title}")
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
# Search and download audio using the AudioFetcher
|
|
392
|
+
fetch_result = self.audio_fetcher.search_and_download(
|
|
393
|
+
artist=self.artist,
|
|
394
|
+
title=self.title,
|
|
395
|
+
output_dir=track_output_dir,
|
|
396
|
+
output_filename=f"{artist_title} (flacfetch)",
|
|
397
|
+
auto_select=self.auto_download,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Update extractor to reflect the actual provider used
|
|
401
|
+
self.extractor = f"flacfetch-{fetch_result.provider}"
|
|
402
|
+
|
|
403
|
+
# Set up the output paths
|
|
404
|
+
output_filename_no_extension = os.path.join(track_output_dir, f"{artist_title} ({self.extractor})")
|
|
405
|
+
|
|
406
|
+
# Copy/move the downloaded file to the expected location
|
|
407
|
+
processed_track["input_media"] = self.file_handler.download_audio_from_fetcher_result(
|
|
408
|
+
fetch_result.filepath, output_filename_no_extension
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
self.logger.info(f"Audio downloaded from {fetch_result.provider}: {processed_track['input_media']}")
|
|
412
|
+
|
|
413
|
+
# Convert to WAV for audio processing
|
|
414
|
+
self.logger.info("Converting downloaded audio to WAV for processing...")
|
|
415
|
+
processed_track["input_audio_wav"] = self.file_handler.convert_to_wav(
|
|
416
|
+
processed_track["input_media"], output_filename_no_extension
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
# No still image for audio-only downloads
|
|
420
|
+
processed_track["input_still_image"] = None
|
|
421
|
+
|
|
422
|
+
except NoResultsError as e:
|
|
423
|
+
self.logger.error(f"No audio found: {e}")
|
|
424
|
+
return None
|
|
425
|
+
except AudioFetcherError as e:
|
|
426
|
+
self.logger.error(f"Failed to fetch audio: {e}")
|
|
427
|
+
return None
|
|
428
|
+
|
|
366
429
|
else:
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
430
|
+
# This case means input_media was None, no audio fetcher flag, and no existing files found
|
|
431
|
+
self.logger.error(f"Cannot proceed: No input file and no existing files found for {artist_title}.")
|
|
432
|
+
self.logger.error("Please provide a local audio file or use artist+title to search for audio.")
|
|
433
|
+
return None
|
|
370
434
|
|
|
371
435
|
if self.skip_lyrics:
|
|
372
436
|
self.logger.info("Skipping lyrics fetch as requested.")
|
|
373
437
|
processed_track["lyrics"] = None
|
|
374
438
|
processed_track["processed_lyrics"] = None
|
|
439
|
+
# No countdown padding when lyrics are skipped
|
|
440
|
+
processed_track["countdown_padding_added"] = False
|
|
441
|
+
processed_track["countdown_padding_seconds"] = 0.0
|
|
442
|
+
processed_track["padded_vocals_audio"] = None
|
|
375
443
|
else:
|
|
376
444
|
lyrics_artist = self.lyrics_artist or self.artist
|
|
377
445
|
lyrics_title = self.lyrics_title or self.title
|
|
@@ -463,6 +531,23 @@ class KaraokePrep:
|
|
|
463
531
|
if isinstance(transcriber_outputs, dict):
|
|
464
532
|
self.lyrics = transcriber_outputs.get("corrected_lyrics_text")
|
|
465
533
|
processed_track["lyrics"] = transcriber_outputs.get("corrected_lyrics_text_filepath")
|
|
534
|
+
|
|
535
|
+
# Capture countdown padding information
|
|
536
|
+
processed_track["countdown_padding_added"] = transcriber_outputs.get("countdown_padding_added", False)
|
|
537
|
+
processed_track["countdown_padding_seconds"] = transcriber_outputs.get("countdown_padding_seconds", 0.0)
|
|
538
|
+
processed_track["padded_vocals_audio"] = transcriber_outputs.get("padded_audio_filepath")
|
|
539
|
+
|
|
540
|
+
# Store ASS filepath for video background processing
|
|
541
|
+
processed_track["ass_filepath"] = transcriber_outputs.get("ass_filepath")
|
|
542
|
+
|
|
543
|
+
if processed_track["countdown_padding_added"]:
|
|
544
|
+
self.logger.info(
|
|
545
|
+
f"=== COUNTDOWN PADDING DETECTED ==="
|
|
546
|
+
)
|
|
547
|
+
self.logger.info(
|
|
548
|
+
f"Vocals have been padded with {processed_track['countdown_padding_seconds']}s of silence. "
|
|
549
|
+
f"Instrumental tracks will be padded after separation to maintain synchronization."
|
|
550
|
+
)
|
|
466
551
|
else:
|
|
467
552
|
self.logger.warning(f"Unexpected type for transcriber_outputs: {type(transcriber_outputs)}, value: {transcriber_outputs}")
|
|
468
553
|
else:
|
|
@@ -476,7 +561,7 @@ class KaraokePrep:
|
|
|
476
561
|
# Check if separation_future was the placeholder or a real task
|
|
477
562
|
# The result index in `results` depends on whether transcription_future existed
|
|
478
563
|
separation_result_index = 1 if transcription_future else 0
|
|
479
|
-
if separation_future is not None and
|
|
564
|
+
if separation_future is not None and isinstance(separation_future, asyncio.Task) and len(results) > separation_result_index:
|
|
480
565
|
self.logger.info("Processing separation results...")
|
|
481
566
|
try:
|
|
482
567
|
separation_results = results[separation_result_index]
|
|
@@ -506,6 +591,78 @@ class KaraokePrep:
|
|
|
506
591
|
|
|
507
592
|
self.logger.info("=== Parallel Processing Complete ===")
|
|
508
593
|
|
|
594
|
+
# Apply video background if requested and lyrics were processed
|
|
595
|
+
if self.video_background_processor and processed_track.get("lyrics"):
|
|
596
|
+
self.logger.info("=== Processing Video Background ===")
|
|
597
|
+
|
|
598
|
+
# Find the With Vocals video file
|
|
599
|
+
with_vocals_video = os.path.join(track_output_dir, f"{artist_title} (With Vocals).mkv")
|
|
600
|
+
|
|
601
|
+
# Get ASS file from transcriber outputs if available
|
|
602
|
+
ass_file = processed_track.get("ass_filepath")
|
|
603
|
+
|
|
604
|
+
# If not in processed_track, try to find it in common locations
|
|
605
|
+
if not ass_file or not os.path.exists(ass_file):
|
|
606
|
+
self.logger.info("ASS filepath not found in transcriber outputs, searching for it...")
|
|
607
|
+
from .utils import sanitize_filename
|
|
608
|
+
sanitized_artist = sanitize_filename(self.artist)
|
|
609
|
+
sanitized_title = sanitize_filename(self.title)
|
|
610
|
+
lyrics_dir = os.path.join(track_output_dir, "lyrics")
|
|
611
|
+
|
|
612
|
+
possible_ass_files = [
|
|
613
|
+
os.path.join(lyrics_dir, f"{sanitized_artist} - {sanitized_title}.ass"),
|
|
614
|
+
os.path.join(track_output_dir, f"{sanitized_artist} - {sanitized_title}.ass"),
|
|
615
|
+
os.path.join(lyrics_dir, f"{artist_title}.ass"),
|
|
616
|
+
os.path.join(track_output_dir, f"{artist_title}.ass"),
|
|
617
|
+
os.path.join(track_output_dir, f"{artist_title} (Karaoke).ass"),
|
|
618
|
+
os.path.join(lyrics_dir, f"{artist_title} (Karaoke).ass"),
|
|
619
|
+
]
|
|
620
|
+
|
|
621
|
+
for possible_file in possible_ass_files:
|
|
622
|
+
if os.path.exists(possible_file):
|
|
623
|
+
ass_file = possible_file
|
|
624
|
+
self.logger.info(f"Found ASS subtitle file: {ass_file}")
|
|
625
|
+
break
|
|
626
|
+
|
|
627
|
+
if os.path.exists(with_vocals_video) and ass_file and os.path.exists(ass_file):
|
|
628
|
+
self.logger.info(f"Found With Vocals video, will replace with video background: {with_vocals_video}")
|
|
629
|
+
self.logger.info(f"Using ASS subtitle file: {ass_file}")
|
|
630
|
+
|
|
631
|
+
# Get audio duration
|
|
632
|
+
audio_duration = self.video_background_processor.get_audio_duration(processed_track["input_audio_wav"])
|
|
633
|
+
|
|
634
|
+
# Check if we need to use the padded audio instead
|
|
635
|
+
if processed_track.get("countdown_padding_added") and processed_track.get("padded_vocals_audio"):
|
|
636
|
+
self.logger.info(f"Using padded vocals audio for video background processing")
|
|
637
|
+
audio_for_video = processed_track["padded_vocals_audio"]
|
|
638
|
+
else:
|
|
639
|
+
audio_for_video = processed_track["input_audio_wav"]
|
|
640
|
+
|
|
641
|
+
# Process video background
|
|
642
|
+
try:
|
|
643
|
+
self.video_background_processor.process_video_background(
|
|
644
|
+
video_path=self.background_video,
|
|
645
|
+
audio_path=audio_for_video,
|
|
646
|
+
ass_subtitles_path=ass_file,
|
|
647
|
+
output_path=with_vocals_video,
|
|
648
|
+
darkness_percent=self.background_video_darkness,
|
|
649
|
+
audio_duration=audio_duration,
|
|
650
|
+
)
|
|
651
|
+
self.logger.info(f"✓ Video background applied, With Vocals video updated: {with_vocals_video}")
|
|
652
|
+
except Exception as e:
|
|
653
|
+
self.logger.error(f"Failed to apply video background: {e}")
|
|
654
|
+
self.logger.exception("Full traceback:")
|
|
655
|
+
# Continue with original video if background processing fails
|
|
656
|
+
else:
|
|
657
|
+
if not os.path.exists(with_vocals_video):
|
|
658
|
+
self.logger.warning(f"With Vocals video not found at {with_vocals_video}, skipping video background processing")
|
|
659
|
+
elif not ass_file or not os.path.exists(ass_file):
|
|
660
|
+
self.logger.warning("Could not find ASS subtitle file, skipping video background processing")
|
|
661
|
+
if 'possible_ass_files' in locals():
|
|
662
|
+
self.logger.warning("Searched locations:")
|
|
663
|
+
for possible_file in possible_ass_files:
|
|
664
|
+
self.logger.warning(f" - {possible_file}")
|
|
665
|
+
|
|
509
666
|
output_image_filepath_noext = os.path.join(track_output_dir, f"{artist_title} (Title)")
|
|
510
667
|
processed_track["title_image_png"] = f"{output_image_filepath_noext}.png"
|
|
511
668
|
processed_track["title_image_jpg"] = f"{output_image_filepath_noext}.jpg"
|
|
@@ -566,16 +723,58 @@ class KaraokePrep:
|
|
|
566
723
|
"instrumental": instrumental_path,
|
|
567
724
|
"vocals": None,
|
|
568
725
|
}
|
|
569
|
-
|
|
570
|
-
#
|
|
571
|
-
if
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
audio_file=processed_track["input_audio_wav"], artist_title=artist_title, track_output_dir=track_output_dir
|
|
726
|
+
|
|
727
|
+
# If countdown padding was added to vocals, pad the custom instrumental too
|
|
728
|
+
if processed_track.get("countdown_padding_added", False):
|
|
729
|
+
padding_seconds = processed_track["countdown_padding_seconds"]
|
|
730
|
+
self.logger.info(
|
|
731
|
+
f"Countdown padding detected - applying {padding_seconds}s padding to custom instrumental"
|
|
576
732
|
)
|
|
577
|
-
|
|
578
|
-
|
|
733
|
+
|
|
734
|
+
base, ext = os.path.splitext(instrumental_path)
|
|
735
|
+
padded_instrumental_path = f"{base} (Padded){ext}"
|
|
736
|
+
|
|
737
|
+
if not self.file_handler._file_exists(padded_instrumental_path):
|
|
738
|
+
self.audio_processor.pad_audio_file(instrumental_path, padded_instrumental_path, padding_seconds)
|
|
739
|
+
|
|
740
|
+
# Update the path to use the padded version
|
|
741
|
+
processed_track["separated_audio"]["Custom"]["instrumental"] = padded_instrumental_path
|
|
742
|
+
self.logger.info(f"✓ Custom instrumental has been padded and synchronized with vocals")
|
|
743
|
+
elif "separated_audio" not in processed_track or not processed_track["separated_audio"]:
|
|
744
|
+
# Only run separation if it wasn't already done in parallel processing
|
|
745
|
+
self.logger.info(f"Separation was not completed in parallel processing, running separation for track: {self.title} by {self.artist}")
|
|
746
|
+
# Delegate to AudioProcessor (called directly, not in thread here)
|
|
747
|
+
separation_results = self.audio_processor.process_audio_separation(
|
|
748
|
+
audio_file=processed_track["input_audio_wav"], artist_title=artist_title, track_output_dir=track_output_dir
|
|
749
|
+
)
|
|
750
|
+
processed_track["separated_audio"] = separation_results
|
|
751
|
+
else:
|
|
752
|
+
self.logger.info("Audio separation was already completed in parallel processing, skipping duplicate separation.")
|
|
753
|
+
|
|
754
|
+
# Apply countdown padding to instrumental files if needed
|
|
755
|
+
if processed_track.get("countdown_padding_added", False):
|
|
756
|
+
padding_seconds = processed_track["countdown_padding_seconds"]
|
|
757
|
+
self.logger.info(
|
|
758
|
+
f"=== APPLYING COUNTDOWN PADDING TO INSTRUMENTALS ==="
|
|
759
|
+
)
|
|
760
|
+
self.logger.info(
|
|
761
|
+
f"Applying {padding_seconds}s padding to all instrumental files to sync with vocal countdown"
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
# Apply padding using AudioProcessor
|
|
765
|
+
padded_separation_result = self.audio_processor.apply_countdown_padding_to_instrumentals(
|
|
766
|
+
separation_result=processed_track["separated_audio"],
|
|
767
|
+
padding_seconds=padding_seconds,
|
|
768
|
+
artist_title=artist_title,
|
|
769
|
+
track_output_dir=track_output_dir,
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
# Update processed_track with padded file paths
|
|
773
|
+
processed_track["separated_audio"] = padded_separation_result
|
|
774
|
+
|
|
775
|
+
self.logger.info(
|
|
776
|
+
f"✓ All instrumental files have been padded and are now synchronized with vocals"
|
|
777
|
+
)
|
|
579
778
|
|
|
580
779
|
self.logger.info("Script finished, audio downloaded, lyrics fetched and audio separated!")
|
|
581
780
|
|
|
@@ -680,21 +879,31 @@ class KaraokePrep:
|
|
|
680
879
|
self.logger.info(f"Input media {self.input_media} is a local folder, processing each file individually...")
|
|
681
880
|
return await self.process_folder()
|
|
682
881
|
elif self.input_media is not None and os.path.isfile(self.input_media):
|
|
683
|
-
self.logger.info(f"Input media {self.input_media} is a local file,
|
|
882
|
+
self.logger.info(f"Input media {self.input_media} is a local file, audio download will be skipped")
|
|
883
|
+
return [await self.prep_single_track()]
|
|
884
|
+
elif self.artist and self.title:
|
|
885
|
+
# No input file provided - use flacfetch to search and download audio
|
|
886
|
+
self.logger.info(f"No input file provided, using flacfetch to search for: {self.artist} - {self.title}")
|
|
887
|
+
|
|
888
|
+
# Set up the extracted_info for metadata consistency
|
|
889
|
+
self.extracted_info = {
|
|
890
|
+
"title": f"{self.artist} - {self.title}",
|
|
891
|
+
"artist": self.artist,
|
|
892
|
+
"track_title": self.title,
|
|
893
|
+
"extractor_key": "flacfetch",
|
|
894
|
+
"id": f"flacfetch_{self.artist}_{self.title}".replace(" ", "_"),
|
|
895
|
+
"url": None,
|
|
896
|
+
"source": "flacfetch",
|
|
897
|
+
}
|
|
898
|
+
self.extractor = "flacfetch"
|
|
899
|
+
self.url = None # URL will be determined by flacfetch
|
|
900
|
+
|
|
901
|
+
# Mark that we need to use audio fetcher for download
|
|
902
|
+
self._use_audio_fetcher = True
|
|
903
|
+
|
|
684
904
|
return [await self.prep_single_track()]
|
|
685
905
|
else:
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
input_url=self.url, input_artist=self.artist, input_title=self.title, logger=self.logger, cookies_str=self.cookies_str
|
|
906
|
+
raise ValueError(
|
|
907
|
+
"Either a local file path or both artist and title must be provided. "
|
|
908
|
+
"URL-based input has been replaced with flacfetch audio fetching."
|
|
690
909
|
)
|
|
691
|
-
|
|
692
|
-
if self.extracted_info and "playlist_count" in self.extracted_info:
|
|
693
|
-
self.persistent_artist = self.artist
|
|
694
|
-
self.logger.info(f"Input URL is a playlist, beginning batch operation with persistent artist: {self.persistent_artist}")
|
|
695
|
-
return await self.process_playlist()
|
|
696
|
-
else:
|
|
697
|
-
self.logger.info(f"Input URL is not a playlist, processing single track")
|
|
698
|
-
# Parse metadata to extract artist and title before processing
|
|
699
|
-
self.parse_single_track_metadata(self.artist, self.title)
|
|
700
|
-
return [await self.prep_single_track()]
|
karaoke_gen/lyrics_processor.py
CHANGED
|
@@ -274,7 +274,8 @@ class LyricsProcessor:
|
|
|
274
274
|
|
|
275
275
|
# Save correction data to JSON file for review interface
|
|
276
276
|
# Use the expected filename format: "{artist} - {title} (Lyrics Corrections).json"
|
|
277
|
-
|
|
277
|
+
# Use sanitized names to be consistent with all other files created by lyrics_transcriber
|
|
278
|
+
corrections_filename = f"{sanitized_artist} - {sanitized_title} (Lyrics Corrections).json"
|
|
278
279
|
corrections_filepath = os.path.join(lyrics_dir, corrections_filename)
|
|
279
280
|
|
|
280
281
|
# Use the CorrectionResult's to_dict() method to serialize
|
|
@@ -285,6 +286,17 @@ class LyricsProcessor:
|
|
|
285
286
|
|
|
286
287
|
self.logger.info(f"Saved correction data to {corrections_filepath}")
|
|
287
288
|
|
|
289
|
+
# Capture countdown padding information for syncing with instrumental audio
|
|
290
|
+
transcriber_outputs["countdown_padding_added"] = getattr(results, "countdown_padding_added", False)
|
|
291
|
+
transcriber_outputs["countdown_padding_seconds"] = getattr(results, "countdown_padding_seconds", 0.0)
|
|
292
|
+
transcriber_outputs["padded_audio_filepath"] = getattr(results, "padded_audio_filepath", None)
|
|
293
|
+
|
|
294
|
+
if transcriber_outputs["countdown_padding_added"]:
|
|
295
|
+
self.logger.info(
|
|
296
|
+
f"Countdown padding detected: {transcriber_outputs['countdown_padding_seconds']}s added to vocals. "
|
|
297
|
+
f"Instrumental audio will need to be padded accordingly."
|
|
298
|
+
)
|
|
299
|
+
|
|
288
300
|
if transcriber_outputs:
|
|
289
301
|
self.logger.info(f"*** Transcriber Filepath Outputs: ***")
|
|
290
302
|
for key, value in transcriber_outputs.items():
|
karaoke_gen/metadata.py
CHANGED
|
@@ -1,86 +1,67 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
|
|
4
4
|
def extract_info_for_online_media(input_url, input_artist, input_title, logger, cookies_str=None):
|
|
5
|
-
"""
|
|
6
|
-
|
|
5
|
+
"""
|
|
6
|
+
Creates metadata info dict from provided artist and title.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"quiet": True,
|
|
11
|
-
# Anti-detection options
|
|
12
|
-
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
13
|
-
"referer": "https://www.youtube.com/",
|
|
14
|
-
"sleep_interval": 1,
|
|
15
|
-
"max_sleep_interval": 3,
|
|
16
|
-
"fragment_retries": 3,
|
|
17
|
-
"extractor_retries": 3,
|
|
18
|
-
"retries": 3,
|
|
19
|
-
# Headers to appear more human
|
|
20
|
-
"http_headers": {
|
|
21
|
-
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
22
|
-
"Accept-Language": "en-us,en;q=0.5",
|
|
23
|
-
"Accept-Encoding": "gzip, deflate",
|
|
24
|
-
"DNT": "1",
|
|
25
|
-
"Connection": "keep-alive",
|
|
26
|
-
"Upgrade-Insecure-Requests": "1",
|
|
27
|
-
},
|
|
28
|
-
}
|
|
8
|
+
Note: This function no longer supports URL-based metadata extraction.
|
|
9
|
+
Audio search and download is now handled by the AudioFetcher class using flacfetch.
|
|
29
10
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
logger.info("Using provided cookies for enhanced YouTube access")
|
|
33
|
-
# Save cookies to a temporary file
|
|
34
|
-
import tempfile
|
|
35
|
-
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
|
36
|
-
f.write(cookies_str)
|
|
37
|
-
base_opts['cookiefile'] = f.name
|
|
38
|
-
else:
|
|
39
|
-
logger.info("No cookies provided - attempting standard extraction")
|
|
11
|
+
When both artist and title are provided, this creates a metadata dict that can be
|
|
12
|
+
used by the rest of the pipeline.
|
|
40
13
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
search_opts.update({
|
|
51
|
-
"format": "bestaudio",
|
|
52
|
-
"noplaylist": "True",
|
|
53
|
-
"extract_flat": True
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
with ydl(search_opts) as ydl_instance:
|
|
57
|
-
query = f"{input_artist} {input_title}"
|
|
58
|
-
search_results = ydl_instance.extract_info(f"ytsearch1:{query}", download=False)
|
|
59
|
-
if search_results and "entries" in search_results and search_results["entries"]:
|
|
60
|
-
extracted_info = search_results["entries"][0]
|
|
61
|
-
else:
|
|
62
|
-
# Raise IndexError to match the expected exception in tests
|
|
63
|
-
raise IndexError(f"No search results found on YouTube for query: {input_artist} {input_title}")
|
|
64
|
-
|
|
65
|
-
if not extracted_info:
|
|
66
|
-
raise Exception(f"Failed to extract info for query: {input_artist} {input_title} or URL: {input_url}")
|
|
67
|
-
|
|
68
|
-
return extracted_info
|
|
14
|
+
Args:
|
|
15
|
+
input_url: Deprecated - URLs should be provided as local file paths or use AudioFetcher
|
|
16
|
+
input_artist: The artist name
|
|
17
|
+
input_title: The track title
|
|
18
|
+
logger: Logger instance
|
|
19
|
+
cookies_str: Deprecated - no longer used
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
A dict with metadata if artist and title are provided
|
|
69
23
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
24
|
+
Raises:
|
|
25
|
+
ValueError: If URL is provided (deprecated) or if artist/title are missing
|
|
26
|
+
"""
|
|
27
|
+
logger.info(f"Extracting info for input_url: {input_url} input_artist: {input_artist} input_title: {input_title}")
|
|
28
|
+
|
|
29
|
+
# URLs are no longer supported - use AudioFetcher for search and download
|
|
30
|
+
if input_url is not None:
|
|
31
|
+
raise ValueError(
|
|
32
|
+
"URL-based audio fetching has been replaced with flacfetch. "
|
|
33
|
+
"Please provide a local file path instead, or use artist and title only "
|
|
34
|
+
"to search for audio via flacfetch."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# When artist and title are provided, create a synthetic metadata dict
|
|
38
|
+
# The actual search and download is handled by AudioFetcher
|
|
39
|
+
if input_artist and input_title:
|
|
40
|
+
logger.info(f"Creating metadata for: {input_artist} - {input_title}")
|
|
41
|
+
return {
|
|
42
|
+
"title": f"{input_artist} - {input_title}",
|
|
43
|
+
"artist": input_artist,
|
|
44
|
+
"track_title": input_title,
|
|
45
|
+
"extractor_key": "flacfetch",
|
|
46
|
+
"id": f"flacfetch_{input_artist}_{input_title}".replace(" ", "_"),
|
|
47
|
+
"url": None, # URL will be determined by flacfetch during download
|
|
48
|
+
"source": "flacfetch",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# No valid input provided
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"Artist and title are required for audio search. "
|
|
54
|
+
f"Received artist: {input_artist}, title: {input_title}"
|
|
55
|
+
)
|
|
78
56
|
|
|
79
57
|
|
|
80
58
|
def parse_track_metadata(extracted_info, current_artist, current_title, persistent_artist, logger):
|
|
81
59
|
"""
|
|
82
60
|
Parses extracted_info to determine URL, extractor, ID, artist, and title.
|
|
83
61
|
Returns a dictionary with the parsed values.
|
|
62
|
+
|
|
63
|
+
This function now supports both legacy yt-dlp style metadata and
|
|
64
|
+
the new flacfetch-based metadata format.
|
|
84
65
|
"""
|
|
85
66
|
parsed_data = {
|
|
86
67
|
"url": None,
|
|
@@ -93,19 +74,43 @@ def parse_track_metadata(extracted_info, current_artist, current_title, persiste
|
|
|
93
74
|
metadata_artist = ""
|
|
94
75
|
metadata_title = ""
|
|
95
76
|
|
|
77
|
+
# Handle flacfetch-style metadata (no URL required)
|
|
78
|
+
if extracted_info.get("source") == "flacfetch":
|
|
79
|
+
parsed_data["url"] = None # URL determined at download time
|
|
80
|
+
parsed_data["extractor"] = "flacfetch"
|
|
81
|
+
parsed_data["media_id"] = extracted_info.get("id")
|
|
82
|
+
|
|
83
|
+
# Use the provided artist/title directly
|
|
84
|
+
if extracted_info.get("artist"):
|
|
85
|
+
parsed_data["artist"] = extracted_info["artist"]
|
|
86
|
+
if extracted_info.get("track_title"):
|
|
87
|
+
parsed_data["title"] = extracted_info["track_title"]
|
|
88
|
+
|
|
89
|
+
if persistent_artist:
|
|
90
|
+
parsed_data["artist"] = persistent_artist
|
|
91
|
+
|
|
92
|
+
logger.info(f"Using flacfetch metadata: artist: {parsed_data['artist']}, title: {parsed_data['title']}")
|
|
93
|
+
return parsed_data
|
|
94
|
+
|
|
95
|
+
# Legacy yt-dlp style metadata handling (for backward compatibility)
|
|
96
96
|
if "url" in extracted_info:
|
|
97
97
|
parsed_data["url"] = extracted_info["url"]
|
|
98
98
|
elif "webpage_url" in extracted_info:
|
|
99
99
|
parsed_data["url"] = extracted_info["webpage_url"]
|
|
100
100
|
else:
|
|
101
|
-
|
|
101
|
+
# For flacfetch results without URL, this is now acceptable
|
|
102
|
+
logger.debug("No URL in extracted info - will be determined at download time")
|
|
103
|
+
parsed_data["url"] = None
|
|
102
104
|
|
|
103
105
|
if "extractor_key" in extracted_info:
|
|
104
106
|
parsed_data["extractor"] = extracted_info["extractor_key"]
|
|
105
107
|
elif "ie_key" in extracted_info:
|
|
106
108
|
parsed_data["extractor"] = extracted_info["ie_key"]
|
|
109
|
+
elif extracted_info.get("source") == "flacfetch":
|
|
110
|
+
parsed_data["extractor"] = "flacfetch"
|
|
107
111
|
else:
|
|
108
|
-
|
|
112
|
+
# Default to flacfetch if no extractor specified
|
|
113
|
+
parsed_data["extractor"] = "flacfetch"
|
|
109
114
|
|
|
110
115
|
if "id" in extracted_info:
|
|
111
116
|
parsed_data["media_id"] = extracted_info["id"]
|
|
@@ -147,7 +152,7 @@ def parse_track_metadata(extracted_info, current_artist, current_title, persiste
|
|
|
147
152
|
parsed_data["artist"] = persistent_artist
|
|
148
153
|
|
|
149
154
|
if parsed_data["artist"] and parsed_data["title"]:
|
|
150
|
-
logger.info(f"
|
|
155
|
+
logger.info(f"Parsed metadata - artist: {parsed_data['artist']}, title: {parsed_data['title']}")
|
|
151
156
|
else:
|
|
152
157
|
logger.debug(extracted_info)
|
|
153
158
|
raise Exception("Failed to extract artist and title from the input media metadata.")
|