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,474 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import logging
|
|
4
|
+
import shutil
|
|
5
|
+
import json
|
|
6
|
+
from lyrics_transcriber import LyricsTranscriber, OutputConfig, TranscriberConfig, LyricsConfig
|
|
7
|
+
from lyrics_transcriber.core.controller import LyricsControllerResult
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
from .utils import sanitize_filename
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Placeholder class or functions for lyrics processing
|
|
13
|
+
class LyricsProcessor:
|
|
14
|
+
# Standard countdown padding duration used by LyricsTranscriber
|
|
15
|
+
COUNTDOWN_PADDING_SECONDS = 3.0
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self, logger, style_params_json, lyrics_file, skip_transcription, skip_transcription_review, render_video, subtitle_offset_ms
|
|
19
|
+
):
|
|
20
|
+
self.logger = logger
|
|
21
|
+
self.style_params_json = style_params_json
|
|
22
|
+
self.lyrics_file = lyrics_file
|
|
23
|
+
self.skip_transcription = skip_transcription
|
|
24
|
+
self.skip_transcription_review = skip_transcription_review
|
|
25
|
+
self.render_video = render_video
|
|
26
|
+
self.subtitle_offset_ms = subtitle_offset_ms
|
|
27
|
+
|
|
28
|
+
def _detect_countdown_padding_from_lrc(self, lrc_filepath):
|
|
29
|
+
"""
|
|
30
|
+
Detect if countdown padding was applied by checking for countdown text in the LRC file.
|
|
31
|
+
|
|
32
|
+
The countdown segment has the text "3... 2... 1..." at timestamp 0.1-2.9s.
|
|
33
|
+
We detect this by looking for the countdown text pattern.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
lrc_filepath: Path to the LRC file
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Tuple of (countdown_padding_added: bool, countdown_padding_seconds: float)
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
with open(lrc_filepath, 'r', encoding='utf-8') as f:
|
|
43
|
+
content = f.read()
|
|
44
|
+
|
|
45
|
+
# Method 1: Check for countdown text pattern "3... 2... 1..."
|
|
46
|
+
# This is the most reliable detection method since the countdown text is unique
|
|
47
|
+
countdown_text = "3... 2... 1..."
|
|
48
|
+
if countdown_text in content:
|
|
49
|
+
self.logger.info(f"Detected countdown padding from LRC: found countdown text '{countdown_text}'")
|
|
50
|
+
return (True, self.COUNTDOWN_PADDING_SECONDS)
|
|
51
|
+
|
|
52
|
+
# Method 2 (fallback): Check if first lyric timestamp is >= 3 seconds
|
|
53
|
+
# This handles cases where countdown text format might differ
|
|
54
|
+
# LRC timestamps: [mm:ss.xx] or [mm:ss.xxx]
|
|
55
|
+
timestamp_pattern = r'\[(\d{1,2}):(\d{2})\.(\d{2,3})\]'
|
|
56
|
+
matches = re.findall(timestamp_pattern, content)
|
|
57
|
+
|
|
58
|
+
if not matches:
|
|
59
|
+
self.logger.debug("No timestamps found in LRC file")
|
|
60
|
+
return (False, 0.0)
|
|
61
|
+
|
|
62
|
+
# Parse the first timestamp
|
|
63
|
+
first_timestamp = matches[0]
|
|
64
|
+
minutes = int(first_timestamp[0])
|
|
65
|
+
seconds = int(first_timestamp[1])
|
|
66
|
+
# Handle both .xx and .xxx formats
|
|
67
|
+
centiseconds = first_timestamp[2]
|
|
68
|
+
if len(centiseconds) == 2:
|
|
69
|
+
milliseconds = int(centiseconds) * 10
|
|
70
|
+
else:
|
|
71
|
+
milliseconds = int(centiseconds)
|
|
72
|
+
|
|
73
|
+
first_lyric_time = minutes * 60 + seconds + milliseconds / 1000.0
|
|
74
|
+
|
|
75
|
+
self.logger.debug(f"First lyric timestamp in LRC: {first_lyric_time:.3f}s")
|
|
76
|
+
|
|
77
|
+
# If first lyric is at or after 3 seconds, countdown padding was applied
|
|
78
|
+
# Use a small buffer (2.5s) to account for songs that naturally start a bit late
|
|
79
|
+
if first_lyric_time >= 2.5:
|
|
80
|
+
self.logger.info(f"Detected countdown padding from LRC: first lyric at {first_lyric_time:.2f}s")
|
|
81
|
+
return (True, self.COUNTDOWN_PADDING_SECONDS)
|
|
82
|
+
|
|
83
|
+
return (False, 0.0)
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
self.logger.warning(f"Failed to detect countdown padding from LRC file: {e}")
|
|
87
|
+
return (False, 0.0)
|
|
88
|
+
|
|
89
|
+
def find_best_split_point(self, line):
|
|
90
|
+
"""
|
|
91
|
+
Find the best split point in a line based on the specified criteria.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
self.logger.debug(f"Finding best_split_point for line: {line}")
|
|
95
|
+
words = line.split()
|
|
96
|
+
mid_word_index = len(words) // 2
|
|
97
|
+
self.logger.debug(f"words: {words} mid_word_index: {mid_word_index}")
|
|
98
|
+
|
|
99
|
+
# Check for a comma within one or two words of the middle word
|
|
100
|
+
if "," in line:
|
|
101
|
+
mid_point = len(" ".join(words[:mid_word_index]))
|
|
102
|
+
comma_indices = [i for i, char in enumerate(line) if char == ","]
|
|
103
|
+
|
|
104
|
+
for index in comma_indices:
|
|
105
|
+
if abs(mid_point - index) < 20 and len(line[: index + 1].strip()) <= 36:
|
|
106
|
+
self.logger.debug(
|
|
107
|
+
f"Found comma at index {index} which is within 20 characters of mid_point {mid_point} and results in a suitable line length, accepting as split point"
|
|
108
|
+
)
|
|
109
|
+
return index + 1 # Include the comma in the first line
|
|
110
|
+
|
|
111
|
+
# Check for 'and'
|
|
112
|
+
if " and " in line:
|
|
113
|
+
mid_point = len(line) // 2
|
|
114
|
+
and_indices = [m.start() for m in re.finditer(" and ", line)]
|
|
115
|
+
for index in sorted(and_indices, key=lambda x: abs(x - mid_point)):
|
|
116
|
+
if len(line[: index + len(" and ")].strip()) <= 36:
|
|
117
|
+
self.logger.debug(f"Found 'and' at index {index} which results in a suitable line length, accepting as split point")
|
|
118
|
+
return index + len(" and ")
|
|
119
|
+
|
|
120
|
+
# If no better split point is found, try splitting at the middle word
|
|
121
|
+
if len(words) > 2 and mid_word_index > 0:
|
|
122
|
+
split_at_middle = len(" ".join(words[:mid_word_index]))
|
|
123
|
+
if split_at_middle <= 36:
|
|
124
|
+
self.logger.debug(f"Splitting at middle word index: {mid_word_index}")
|
|
125
|
+
return split_at_middle
|
|
126
|
+
|
|
127
|
+
# If the line is still too long, forcibly split at the maximum length
|
|
128
|
+
forced_split_point = 36
|
|
129
|
+
if len(line) > forced_split_point:
|
|
130
|
+
self.logger.debug(f"Line is still too long, forcibly splitting at position {forced_split_point}")
|
|
131
|
+
return forced_split_point
|
|
132
|
+
|
|
133
|
+
def process_line(self, line):
|
|
134
|
+
"""
|
|
135
|
+
Process a single line to ensure it's within the maximum length,
|
|
136
|
+
and handle parentheses.
|
|
137
|
+
"""
|
|
138
|
+
processed_lines = []
|
|
139
|
+
iteration_count = 0
|
|
140
|
+
max_iterations = 100 # Failsafe limit
|
|
141
|
+
|
|
142
|
+
while len(line) > 36:
|
|
143
|
+
if iteration_count > max_iterations:
|
|
144
|
+
self.logger.error(f"Maximum iterations exceeded in process_line for line: {line}")
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
# Check if the line contains parentheses
|
|
148
|
+
if "(" in line and ")" in line:
|
|
149
|
+
start_paren = line.find("(")
|
|
150
|
+
end_paren = line.find(")") + 1
|
|
151
|
+
if end_paren < len(line) and line[end_paren] == ",":
|
|
152
|
+
end_paren += 1
|
|
153
|
+
|
|
154
|
+
if start_paren > 0:
|
|
155
|
+
processed_lines.append(line[:start_paren].strip())
|
|
156
|
+
processed_lines.append(line[start_paren:end_paren].strip())
|
|
157
|
+
line = line[end_paren:].strip()
|
|
158
|
+
else:
|
|
159
|
+
split_point = self.find_best_split_point(line)
|
|
160
|
+
processed_lines.append(line[:split_point].strip())
|
|
161
|
+
line = line[split_point:].strip()
|
|
162
|
+
|
|
163
|
+
iteration_count += 1
|
|
164
|
+
|
|
165
|
+
if line: # Add the remaining part if not empty
|
|
166
|
+
processed_lines.append(line)
|
|
167
|
+
|
|
168
|
+
return processed_lines
|
|
169
|
+
|
|
170
|
+
def _check_transcription_providers(self) -> dict:
|
|
171
|
+
"""
|
|
172
|
+
Check which transcription providers are configured and return their status.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
dict with 'configured' (list of provider names) and 'missing' (list of missing configs)
|
|
176
|
+
"""
|
|
177
|
+
load_dotenv()
|
|
178
|
+
|
|
179
|
+
configured = []
|
|
180
|
+
missing = []
|
|
181
|
+
|
|
182
|
+
# Check AudioShake
|
|
183
|
+
audioshake_token = os.getenv("AUDIOSHAKE_API_TOKEN")
|
|
184
|
+
if audioshake_token:
|
|
185
|
+
configured.append("AudioShake")
|
|
186
|
+
self.logger.debug("AudioShake transcription provider: configured")
|
|
187
|
+
else:
|
|
188
|
+
missing.append("AudioShake (AUDIOSHAKE_API_TOKEN)")
|
|
189
|
+
self.logger.debug("AudioShake transcription provider: not configured (missing AUDIOSHAKE_API_TOKEN)")
|
|
190
|
+
|
|
191
|
+
# Check Whisper via RunPod
|
|
192
|
+
runpod_key = os.getenv("RUNPOD_API_KEY")
|
|
193
|
+
whisper_id = os.getenv("WHISPER_RUNPOD_ID")
|
|
194
|
+
if runpod_key and whisper_id:
|
|
195
|
+
configured.append("Whisper (RunPod)")
|
|
196
|
+
self.logger.debug("Whisper transcription provider: configured")
|
|
197
|
+
elif runpod_key:
|
|
198
|
+
missing.append("Whisper (missing WHISPER_RUNPOD_ID)")
|
|
199
|
+
self.logger.debug("Whisper transcription provider: partially configured (missing WHISPER_RUNPOD_ID)")
|
|
200
|
+
elif whisper_id:
|
|
201
|
+
missing.append("Whisper (missing RUNPOD_API_KEY)")
|
|
202
|
+
self.logger.debug("Whisper transcription provider: partially configured (missing RUNPOD_API_KEY)")
|
|
203
|
+
else:
|
|
204
|
+
missing.append("Whisper (RUNPOD_API_KEY + WHISPER_RUNPOD_ID)")
|
|
205
|
+
self.logger.debug("Whisper transcription provider: not configured")
|
|
206
|
+
|
|
207
|
+
return {"configured": configured, "missing": missing}
|
|
208
|
+
|
|
209
|
+
def _build_transcription_provider_error_message(self, missing_providers: list) -> str:
|
|
210
|
+
"""Build a helpful error message when no transcription providers are configured."""
|
|
211
|
+
return (
|
|
212
|
+
"No transcription providers configured!\n"
|
|
213
|
+
"\n"
|
|
214
|
+
"Karaoke video generation requires at least one transcription provider to create "
|
|
215
|
+
"synchronized lyrics. Without a transcription provider, the system cannot generate "
|
|
216
|
+
"the word-level timing data needed for the karaoke video.\n"
|
|
217
|
+
"\n"
|
|
218
|
+
"AVAILABLE TRANSCRIPTION PROVIDERS:\n"
|
|
219
|
+
"\n"
|
|
220
|
+
"1. AudioShake (Recommended - Commercial, high-quality)\n"
|
|
221
|
+
" - Set environment variable: AUDIOSHAKE_API_TOKEN=your_token\n"
|
|
222
|
+
" - Get an API key at: https://www.audioshake.ai/\n"
|
|
223
|
+
"\n"
|
|
224
|
+
"2. Whisper via RunPod (Open-source alternative)\n"
|
|
225
|
+
" - Set environment variables:\n"
|
|
226
|
+
" RUNPOD_API_KEY=your_key\n"
|
|
227
|
+
" WHISPER_RUNPOD_ID=your_endpoint_id\n"
|
|
228
|
+
" - Set up a Whisper endpoint at: https://www.runpod.io/\n"
|
|
229
|
+
"\n"
|
|
230
|
+
"ALTERNATIVES:\n"
|
|
231
|
+
"\n"
|
|
232
|
+
"- Use --skip-lyrics flag to generate instrumental-only karaoke (no synchronized lyrics)\n"
|
|
233
|
+
"- Use --lyrics_file to provide pre-timed lyrics (still needs transcription for timing)\n"
|
|
234
|
+
"\n"
|
|
235
|
+
f"Missing provider configurations: {', '.join(missing_providers)}\n"
|
|
236
|
+
"\n"
|
|
237
|
+
"See README.md 'Transcription Providers' section for detailed setup instructions."
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def transcribe_lyrics(self, input_audio_wav, artist, title, track_output_dir, lyrics_artist=None, lyrics_title=None):
|
|
241
|
+
"""
|
|
242
|
+
Transcribe lyrics for a track.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
input_audio_wav: Path to the audio file
|
|
246
|
+
artist: Original artist name (used for filename generation)
|
|
247
|
+
title: Original title (used for filename generation)
|
|
248
|
+
track_output_dir: Output directory path
|
|
249
|
+
lyrics_artist: Artist name for lyrics processing (defaults to artist if None)
|
|
250
|
+
lyrics_title: Title for lyrics processing (defaults to title if None)
|
|
251
|
+
|
|
252
|
+
Raises:
|
|
253
|
+
ValueError: If transcription is enabled but no providers are configured
|
|
254
|
+
"""
|
|
255
|
+
# Use original artist/title for filename generation
|
|
256
|
+
filename_artist = artist
|
|
257
|
+
filename_title = title
|
|
258
|
+
|
|
259
|
+
# Use lyrics_artist/lyrics_title for actual lyrics processing, fall back to originals if not provided
|
|
260
|
+
processing_artist = lyrics_artist or artist
|
|
261
|
+
processing_title = lyrics_title or title
|
|
262
|
+
|
|
263
|
+
self.logger.info(
|
|
264
|
+
f"Transcribing lyrics for track {processing_artist} - {processing_title} from audio file: {input_audio_wav} with output directory: {track_output_dir}"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Check for existing files first using sanitized names from ORIGINAL artist/title for consistency
|
|
268
|
+
sanitized_artist = sanitize_filename(filename_artist)
|
|
269
|
+
sanitized_title = sanitize_filename(filename_title)
|
|
270
|
+
parent_video_path = os.path.join(track_output_dir, f"{sanitized_artist} - {sanitized_title} (With Vocals).mkv")
|
|
271
|
+
parent_lrc_path = os.path.join(track_output_dir, f"{sanitized_artist} - {sanitized_title} (Karaoke).lrc")
|
|
272
|
+
|
|
273
|
+
# Check lyrics directory for existing files
|
|
274
|
+
lyrics_dir = os.path.join(track_output_dir, "lyrics")
|
|
275
|
+
lyrics_video_path = os.path.join(lyrics_dir, f"{sanitized_artist} - {sanitized_title} (With Vocals).mkv")
|
|
276
|
+
lyrics_lrc_path = os.path.join(lyrics_dir, f"{sanitized_artist} - {sanitized_title} (Karaoke).lrc")
|
|
277
|
+
|
|
278
|
+
# If files exist in parent directory, return early (but detect countdown padding first)
|
|
279
|
+
if os.path.exists(parent_video_path) and os.path.exists(parent_lrc_path):
|
|
280
|
+
self.logger.info("Found existing video and LRC files in parent directory, skipping transcription")
|
|
281
|
+
|
|
282
|
+
# Detect countdown padding from existing LRC file
|
|
283
|
+
countdown_padding_added, countdown_padding_seconds = self._detect_countdown_padding_from_lrc(parent_lrc_path)
|
|
284
|
+
|
|
285
|
+
if countdown_padding_added:
|
|
286
|
+
self.logger.info(f"Existing files have countdown padding: {countdown_padding_seconds}s")
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
"lrc_filepath": parent_lrc_path,
|
|
290
|
+
"ass_filepath": parent_video_path,
|
|
291
|
+
"countdown_padding_added": countdown_padding_added,
|
|
292
|
+
"countdown_padding_seconds": countdown_padding_seconds,
|
|
293
|
+
"padded_audio_filepath": None, # Original padded audio may not exist
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# If files exist in lyrics directory, copy to parent and return (but detect countdown padding first)
|
|
297
|
+
if os.path.exists(lyrics_video_path) and os.path.exists(lyrics_lrc_path):
|
|
298
|
+
self.logger.info("Found existing video and LRC files in lyrics directory, copying to parent")
|
|
299
|
+
os.makedirs(track_output_dir, exist_ok=True)
|
|
300
|
+
shutil.copy2(lyrics_video_path, parent_video_path)
|
|
301
|
+
shutil.copy2(lyrics_lrc_path, parent_lrc_path)
|
|
302
|
+
|
|
303
|
+
# Detect countdown padding from existing LRC file
|
|
304
|
+
countdown_padding_added, countdown_padding_seconds = self._detect_countdown_padding_from_lrc(parent_lrc_path)
|
|
305
|
+
|
|
306
|
+
if countdown_padding_added:
|
|
307
|
+
self.logger.info(f"Existing files have countdown padding: {countdown_padding_seconds}s")
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
"lrc_filepath": parent_lrc_path,
|
|
311
|
+
"ass_filepath": parent_video_path,
|
|
312
|
+
"countdown_padding_added": countdown_padding_added,
|
|
313
|
+
"countdown_padding_seconds": countdown_padding_seconds,
|
|
314
|
+
"padded_audio_filepath": None, # Original padded audio may not exist
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
# Check transcription provider configuration if transcription is not being skipped
|
|
318
|
+
# Do this AFTER checking for existing files, since existing files don't need transcription
|
|
319
|
+
if not self.skip_transcription:
|
|
320
|
+
provider_status = self._check_transcription_providers()
|
|
321
|
+
|
|
322
|
+
if provider_status["configured"]:
|
|
323
|
+
self.logger.info(f"Transcription providers configured: {', '.join(provider_status['configured'])}")
|
|
324
|
+
else:
|
|
325
|
+
error_msg = self._build_transcription_provider_error_message(provider_status["missing"])
|
|
326
|
+
raise ValueError(error_msg)
|
|
327
|
+
|
|
328
|
+
# Create lyrics directory if it doesn't exist
|
|
329
|
+
os.makedirs(lyrics_dir, exist_ok=True)
|
|
330
|
+
self.logger.info(f"Created lyrics directory: {lyrics_dir}")
|
|
331
|
+
|
|
332
|
+
# Set render_video to False if explicitly disabled
|
|
333
|
+
render_video = self.render_video
|
|
334
|
+
if not render_video:
|
|
335
|
+
self.logger.info("Video rendering disabled, skipping video output")
|
|
336
|
+
|
|
337
|
+
# Load environment variables
|
|
338
|
+
load_dotenv()
|
|
339
|
+
env_config = {
|
|
340
|
+
"audioshake_api_token": os.getenv("AUDIOSHAKE_API_TOKEN"),
|
|
341
|
+
"genius_api_token": os.getenv("GENIUS_API_TOKEN"),
|
|
342
|
+
"spotify_cookie": os.getenv("SPOTIFY_COOKIE_SP_DC"),
|
|
343
|
+
"runpod_api_key": os.getenv("RUNPOD_API_KEY"),
|
|
344
|
+
"whisper_runpod_id": os.getenv("WHISPER_RUNPOD_ID"),
|
|
345
|
+
"rapidapi_key": os.getenv("RAPIDAPI_KEY"), # Add missing RAPIDAPI_KEY
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
# Create config objects for LyricsTranscriber
|
|
349
|
+
transcriber_config = TranscriberConfig(
|
|
350
|
+
audioshake_api_token=env_config.get("audioshake_api_token"),
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
lyrics_config = LyricsConfig(
|
|
354
|
+
genius_api_token=env_config.get("genius_api_token"),
|
|
355
|
+
spotify_cookie=env_config.get("spotify_cookie"),
|
|
356
|
+
rapidapi_key=env_config.get("rapidapi_key"),
|
|
357
|
+
lyrics_file=self.lyrics_file,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Debug logging for lyrics_config
|
|
361
|
+
self.logger.info(f"LyricsConfig created with:")
|
|
362
|
+
self.logger.info(f" genius_api_token: {env_config.get('genius_api_token')[:3] + '...' if env_config.get('genius_api_token') else 'None'}")
|
|
363
|
+
self.logger.info(f" spotify_cookie: {env_config.get('spotify_cookie')[:3] + '...' if env_config.get('spotify_cookie') else 'None'}")
|
|
364
|
+
self.logger.info(f" rapidapi_key: {env_config.get('rapidapi_key')[:3] + '...' if env_config.get('rapidapi_key') else 'None'}")
|
|
365
|
+
self.logger.info(f" lyrics_file: {self.lyrics_file}")
|
|
366
|
+
|
|
367
|
+
# Detect if we're running in a serverless environment (Modal)
|
|
368
|
+
# Modal sets specific environment variables we can check for
|
|
369
|
+
is_serverless = (
|
|
370
|
+
os.getenv("MODAL_TASK_ID") is not None or
|
|
371
|
+
os.getenv("MODAL_FUNCTION_NAME") is not None or
|
|
372
|
+
os.path.exists("/.modal") # Modal creates this directory in containers
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# In serverless environment, disable interactive review even if skip_transcription_review=False
|
|
376
|
+
# This preserves CLI behavior while fixing serverless hanging
|
|
377
|
+
enable_review_setting = not self.skip_transcription_review and not is_serverless
|
|
378
|
+
|
|
379
|
+
if is_serverless and not self.skip_transcription_review:
|
|
380
|
+
self.logger.info("Detected serverless environment - disabling interactive review to prevent hanging")
|
|
381
|
+
|
|
382
|
+
# In serverless environment, disable video generation during Phase 1 to save compute
|
|
383
|
+
# Video will be generated in Phase 2 after human review
|
|
384
|
+
serverless_render_video = render_video and not is_serverless
|
|
385
|
+
|
|
386
|
+
if is_serverless and render_video:
|
|
387
|
+
self.logger.info("Detected serverless environment - deferring video generation until after review")
|
|
388
|
+
|
|
389
|
+
output_config = OutputConfig(
|
|
390
|
+
output_styles_json=self.style_params_json,
|
|
391
|
+
output_dir=lyrics_dir,
|
|
392
|
+
render_video=serverless_render_video, # Disable video in serverless Phase 1
|
|
393
|
+
fetch_lyrics=True,
|
|
394
|
+
run_transcription=not self.skip_transcription,
|
|
395
|
+
run_correction=True,
|
|
396
|
+
generate_plain_text=True,
|
|
397
|
+
generate_lrc=True,
|
|
398
|
+
generate_cdg=False, # Also defer CDG generation to Phase 2
|
|
399
|
+
video_resolution="4k",
|
|
400
|
+
enable_review=enable_review_setting,
|
|
401
|
+
subtitle_offset_ms=self.subtitle_offset_ms,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Add this log entry to debug the OutputConfig
|
|
405
|
+
self.logger.info(f"Instantiating LyricsTranscriber with OutputConfig: {output_config}")
|
|
406
|
+
|
|
407
|
+
# Initialize transcriber with new config objects - use PROCESSING artist/title for lyrics work
|
|
408
|
+
transcriber = LyricsTranscriber(
|
|
409
|
+
audio_filepath=input_audio_wav,
|
|
410
|
+
artist=processing_artist, # Use lyrics_artist for processing
|
|
411
|
+
title=processing_title, # Use lyrics_title for processing
|
|
412
|
+
transcriber_config=transcriber_config,
|
|
413
|
+
lyrics_config=lyrics_config,
|
|
414
|
+
output_config=output_config,
|
|
415
|
+
logger=self.logger,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Process and get results
|
|
419
|
+
results: LyricsControllerResult = transcriber.process()
|
|
420
|
+
self.logger.info(f"Transcriber Results Filepaths:")
|
|
421
|
+
for key, value in results.__dict__.items():
|
|
422
|
+
if key.endswith("_filepath"):
|
|
423
|
+
self.logger.info(f" {key}: {value}")
|
|
424
|
+
|
|
425
|
+
# Build output dictionary
|
|
426
|
+
transcriber_outputs = {}
|
|
427
|
+
if results.lrc_filepath:
|
|
428
|
+
transcriber_outputs["lrc_filepath"] = results.lrc_filepath
|
|
429
|
+
self.logger.info(f"Moving LRC file from {results.lrc_filepath} to {parent_lrc_path}")
|
|
430
|
+
shutil.copy2(results.lrc_filepath, parent_lrc_path)
|
|
431
|
+
|
|
432
|
+
if results.ass_filepath:
|
|
433
|
+
transcriber_outputs["ass_filepath"] = results.ass_filepath
|
|
434
|
+
self.logger.info(f"Moving video file from {results.video_filepath} to {parent_video_path}")
|
|
435
|
+
shutil.copy2(results.video_filepath, parent_video_path)
|
|
436
|
+
|
|
437
|
+
if results.transcription_corrected:
|
|
438
|
+
transcriber_outputs["corrected_lyrics_text"] = "\n".join(
|
|
439
|
+
segment.text for segment in results.transcription_corrected.corrected_segments
|
|
440
|
+
)
|
|
441
|
+
transcriber_outputs["corrected_lyrics_text_filepath"] = results.corrected_txt
|
|
442
|
+
|
|
443
|
+
# Save correction data to JSON file for review interface
|
|
444
|
+
# Use the expected filename format: "{artist} - {title} (Lyrics Corrections).json"
|
|
445
|
+
# Use sanitized names to be consistent with all other files created by lyrics_transcriber
|
|
446
|
+
corrections_filename = f"{sanitized_artist} - {sanitized_title} (Lyrics Corrections).json"
|
|
447
|
+
corrections_filepath = os.path.join(lyrics_dir, corrections_filename)
|
|
448
|
+
|
|
449
|
+
# Use the CorrectionResult's to_dict() method to serialize
|
|
450
|
+
correction_data = results.transcription_corrected.to_dict()
|
|
451
|
+
|
|
452
|
+
with open(corrections_filepath, 'w') as f:
|
|
453
|
+
json.dump(correction_data, f, indent=2)
|
|
454
|
+
|
|
455
|
+
self.logger.info(f"Saved correction data to {corrections_filepath}")
|
|
456
|
+
|
|
457
|
+
# Capture countdown padding information for syncing with instrumental audio
|
|
458
|
+
transcriber_outputs["countdown_padding_added"] = getattr(results, "countdown_padding_added", False)
|
|
459
|
+
transcriber_outputs["countdown_padding_seconds"] = getattr(results, "countdown_padding_seconds", 0.0)
|
|
460
|
+
transcriber_outputs["padded_audio_filepath"] = getattr(results, "padded_audio_filepath", None)
|
|
461
|
+
|
|
462
|
+
if transcriber_outputs["countdown_padding_added"]:
|
|
463
|
+
self.logger.info(
|
|
464
|
+
f"Countdown padding detected: {transcriber_outputs['countdown_padding_seconds']}s added to vocals. "
|
|
465
|
+
f"Instrumental audio will need to be padded accordingly."
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
if transcriber_outputs:
|
|
469
|
+
self.logger.info(f"*** Transcriber Filepath Outputs: ***")
|
|
470
|
+
for key, value in transcriber_outputs.items():
|
|
471
|
+
if key.endswith("_filepath"):
|
|
472
|
+
self.logger.info(f" {key}: {value}")
|
|
473
|
+
|
|
474
|
+
return transcriber_outputs
|
karaoke_gen/metadata.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def extract_info_for_online_media(input_url, input_artist, input_title, logger, cookies_str=None):
|
|
5
|
+
"""
|
|
6
|
+
Creates metadata info dict from provided artist and title.
|
|
7
|
+
|
|
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.
|
|
10
|
+
|
|
11
|
+
When both artist and title are provided, this creates a metadata dict that can be
|
|
12
|
+
used by the rest of the pipeline.
|
|
13
|
+
|
|
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
|
|
23
|
+
|
|
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
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse_track_metadata(extracted_info, current_artist, current_title, persistent_artist, logger):
|
|
59
|
+
"""
|
|
60
|
+
Parses extracted_info to determine URL, extractor, ID, artist, and title.
|
|
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.
|
|
65
|
+
"""
|
|
66
|
+
parsed_data = {
|
|
67
|
+
"url": None,
|
|
68
|
+
"extractor": None,
|
|
69
|
+
"media_id": None,
|
|
70
|
+
"artist": current_artist,
|
|
71
|
+
"title": current_title,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
metadata_artist = ""
|
|
75
|
+
metadata_title = ""
|
|
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
|
+
if "url" in extracted_info:
|
|
97
|
+
parsed_data["url"] = extracted_info["url"]
|
|
98
|
+
elif "webpage_url" in extracted_info:
|
|
99
|
+
parsed_data["url"] = extracted_info["webpage_url"]
|
|
100
|
+
else:
|
|
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
|
|
104
|
+
|
|
105
|
+
if "extractor_key" in extracted_info:
|
|
106
|
+
parsed_data["extractor"] = extracted_info["extractor_key"]
|
|
107
|
+
elif "ie_key" in extracted_info:
|
|
108
|
+
parsed_data["extractor"] = extracted_info["ie_key"]
|
|
109
|
+
elif extracted_info.get("source") == "flacfetch":
|
|
110
|
+
parsed_data["extractor"] = "flacfetch"
|
|
111
|
+
else:
|
|
112
|
+
# Default to flacfetch if no extractor specified
|
|
113
|
+
parsed_data["extractor"] = "flacfetch"
|
|
114
|
+
|
|
115
|
+
if "id" in extracted_info:
|
|
116
|
+
parsed_data["media_id"] = extracted_info["id"]
|
|
117
|
+
|
|
118
|
+
# Example: "Artist - Title"
|
|
119
|
+
if "title" in extracted_info and "-" in extracted_info["title"]:
|
|
120
|
+
try:
|
|
121
|
+
metadata_artist, metadata_title = extracted_info["title"].split("-", 1)
|
|
122
|
+
metadata_artist = metadata_artist.strip()
|
|
123
|
+
metadata_title = metadata_title.strip()
|
|
124
|
+
except ValueError:
|
|
125
|
+
logger.warning(f"Could not split title '{extracted_info['title']}' on '-', using full title.")
|
|
126
|
+
metadata_title = extracted_info["title"].strip()
|
|
127
|
+
if "uploader" in extracted_info:
|
|
128
|
+
metadata_artist = extracted_info["uploader"]
|
|
129
|
+
|
|
130
|
+
elif "uploader" in extracted_info:
|
|
131
|
+
# Fallback to uploader as artist if title parsing fails
|
|
132
|
+
metadata_artist = extracted_info["uploader"]
|
|
133
|
+
if "title" in extracted_info:
|
|
134
|
+
metadata_title = extracted_info["title"].strip()
|
|
135
|
+
|
|
136
|
+
# If unable to parse, log an appropriate message
|
|
137
|
+
if not metadata_artist or not metadata_title:
|
|
138
|
+
logger.warning("Could not parse artist and title from the input media metadata.")
|
|
139
|
+
|
|
140
|
+
if not parsed_data["artist"] and metadata_artist:
|
|
141
|
+
logger.warning(f"Artist not provided as input, setting to {metadata_artist} from input media metadata...")
|
|
142
|
+
parsed_data["artist"] = metadata_artist
|
|
143
|
+
|
|
144
|
+
if not parsed_data["title"] and metadata_title:
|
|
145
|
+
logger.warning(f"Title not provided as input, setting to {metadata_title} from input media metadata...")
|
|
146
|
+
parsed_data["title"] = metadata_title
|
|
147
|
+
|
|
148
|
+
if persistent_artist:
|
|
149
|
+
logger.debug(
|
|
150
|
+
f"Resetting artist from {parsed_data['artist']} to persistent artist: {persistent_artist} for consistency while processing playlist..."
|
|
151
|
+
)
|
|
152
|
+
parsed_data["artist"] = persistent_artist
|
|
153
|
+
|
|
154
|
+
if parsed_data["artist"] and parsed_data["title"]:
|
|
155
|
+
logger.info(f"Parsed metadata - artist: {parsed_data['artist']}, title: {parsed_data['title']}")
|
|
156
|
+
else:
|
|
157
|
+
logger.debug(extracted_info)
|
|
158
|
+
raise Exception("Failed to extract artist and title from the input media metadata.")
|
|
159
|
+
|
|
160
|
+
return parsed_data
|