karaoke-gen 0.75.16__py3-none-any.whl → 0.76.20__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 +984 -33
- karaoke_gen/audio_processor.py +4 -0
- karaoke_gen/instrumental_review/static/index.html +37 -14
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +25 -1
- karaoke_gen/karaoke_gen.py +208 -39
- karaoke_gen/lyrics_processor.py +111 -31
- karaoke_gen/utils/__init__.py +26 -0
- karaoke_gen/utils/cli_args.py +15 -6
- karaoke_gen/utils/gen_cli.py +30 -5
- karaoke_gen/utils/remote_cli.py +301 -20
- {karaoke_gen-0.75.16.dist-info → karaoke_gen-0.76.20.dist-info}/METADATA +107 -5
- {karaoke_gen-0.75.16.dist-info → karaoke_gen-0.76.20.dist-info}/RECORD +47 -43
- lyrics_transcriber/core/controller.py +76 -2
- lyrics_transcriber/frontend/index.html +5 -1
- lyrics_transcriber/frontend/package-lock.json +4553 -0
- lyrics_transcriber/frontend/package.json +4 -1
- lyrics_transcriber/frontend/playwright.config.ts +69 -0
- lyrics_transcriber/frontend/public/nomad-karaoke-logo.svg +5 -0
- lyrics_transcriber/frontend/src/App.tsx +94 -63
- lyrics_transcriber/frontend/src/api.ts +25 -10
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +55 -21
- lyrics_transcriber/frontend/src/components/AppHeader.tsx +65 -0
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +5 -5
- lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +9 -9
- lyrics_transcriber/frontend/src/components/EditModal.tsx +1 -1
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +1 -1
- lyrics_transcriber/frontend/src/components/Header.tsx +34 -48
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +22 -21
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +3 -3
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +2 -2
- lyrics_transcriber/frontend/src/components/shared/constants.ts +15 -5
- lyrics_transcriber/frontend/src/main.tsx +1 -7
- lyrics_transcriber/frontend/src/theme.ts +337 -135
- lyrics_transcriber/frontend/vite.config.ts +5 -0
- lyrics_transcriber/frontend/web_assets/assets/{index-COYImAcx.js → index-BECn1o8Q.js} +38 -22
- lyrics_transcriber/frontend/web_assets/assets/{index-COYImAcx.js.map → index-BECn1o8Q.js.map} +1 -1
- lyrics_transcriber/frontend/web_assets/index.html +1 -1
- lyrics_transcriber/frontend/yarn.lock +1005 -1046
- lyrics_transcriber/output/countdown_processor.py +39 -0
- lyrics_transcriber/review/server.py +1 -1
- lyrics_transcriber/transcribers/audioshake.py +96 -7
- lyrics_transcriber/types.py +14 -12
- {karaoke_gen-0.75.16.dist-info → karaoke_gen-0.76.20.dist-info}/WHEEL +0 -0
- {karaoke_gen-0.75.16.dist-info → karaoke_gen-0.76.20.dist-info}/entry_points.txt +0 -0
- {karaoke_gen-0.75.16.dist-info → karaoke_gen-0.76.20.dist-info}/licenses/LICENSE +0 -0
karaoke_gen/audio_processor.py
CHANGED
|
@@ -771,6 +771,10 @@ class AudioProcessor:
|
|
|
771
771
|
padded_result["other_stems"] = separation_result.get("other_stems", {})
|
|
772
772
|
padded_result["backing_vocals"] = separation_result.get("backing_vocals", {})
|
|
773
773
|
|
|
774
|
+
# Preserve Custom instrumental if present (already padded in karaoke_gen.py)
|
|
775
|
+
if "Custom" in separation_result:
|
|
776
|
+
padded_result["Custom"] = separation_result["Custom"]
|
|
777
|
+
|
|
774
778
|
# Count actual padded files (don't assume clean instrumental was padded)
|
|
775
779
|
padded_count = (1 if padded_result["clean_instrumental"].get("instrumental") else 0) + len(padded_result["combined_instrumentals"])
|
|
776
780
|
|
|
@@ -598,7 +598,22 @@
|
|
|
598
598
|
let animationFrameId = null;
|
|
599
599
|
let currentAudioElement = null; // Track audio element reference for listener management
|
|
600
600
|
|
|
601
|
-
|
|
601
|
+
// Parse URL parameters for cloud mode
|
|
602
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
603
|
+
const encodedBaseApiUrl = urlParams.get('baseApiUrl');
|
|
604
|
+
const instrumentalToken = urlParams.get('instrumentalToken');
|
|
605
|
+
|
|
606
|
+
// Determine API base URL - cloud mode uses provided URL, local mode uses default
|
|
607
|
+
const API_BASE = encodedBaseApiUrl
|
|
608
|
+
? decodeURIComponent(encodedBaseApiUrl)
|
|
609
|
+
: '/api/jobs/local';
|
|
610
|
+
|
|
611
|
+
// Helper to add token to URL if available
|
|
612
|
+
function addTokenToUrl(url) {
|
|
613
|
+
if (!instrumentalToken) return url;
|
|
614
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
615
|
+
return `${url}${separator}instrumental_token=${encodeURIComponent(instrumentalToken)}`;
|
|
616
|
+
}
|
|
602
617
|
|
|
603
618
|
// HTML escape helper to prevent XSS
|
|
604
619
|
function escapeHtml(str) {
|
|
@@ -617,8 +632,8 @@
|
|
|
617
632
|
async function init() {
|
|
618
633
|
try {
|
|
619
634
|
const [analysisRes, waveformRes] = await Promise.all([
|
|
620
|
-
fetch(`${API_BASE}/instrumental-analysis`),
|
|
621
|
-
fetch(`${API_BASE}/waveform-data?num_points=1000`)
|
|
635
|
+
fetch(addTokenToUrl(`${API_BASE}/instrumental-analysis`)),
|
|
636
|
+
fetch(addTokenToUrl(`${API_BASE}/waveform-data?num_points=1000`))
|
|
622
637
|
]);
|
|
623
638
|
|
|
624
639
|
if (!analysisRes.ok) throw new Error('Failed to load analysis');
|
|
@@ -1120,15 +1135,23 @@
|
|
|
1120
1135
|
}
|
|
1121
1136
|
|
|
1122
1137
|
function getAudioUrl() {
|
|
1123
|
-
const
|
|
1124
|
-
original: '
|
|
1125
|
-
backing: '
|
|
1126
|
-
clean: '
|
|
1127
|
-
with_backing: '
|
|
1128
|
-
custom: '
|
|
1129
|
-
uploaded: '
|
|
1138
|
+
const stemTypes = {
|
|
1139
|
+
original: 'original',
|
|
1140
|
+
backing: 'backing_vocals',
|
|
1141
|
+
clean: 'clean_instrumental',
|
|
1142
|
+
with_backing: 'with_backing',
|
|
1143
|
+
custom: 'custom_instrumental',
|
|
1144
|
+
uploaded: 'uploaded_instrumental'
|
|
1130
1145
|
};
|
|
1131
|
-
|
|
1146
|
+
const stemType = stemTypes[activeAudio] || stemTypes.backing;
|
|
1147
|
+
|
|
1148
|
+
// Cloud mode uses /audio-stream/{stem_type}, local mode uses /api/audio/{stem_type}
|
|
1149
|
+
const isCloudMode = !!encodedBaseApiUrl;
|
|
1150
|
+
const url = isCloudMode
|
|
1151
|
+
? `${API_BASE}/audio-stream/${stemType}`
|
|
1152
|
+
: `/api/audio/${stemType}`;
|
|
1153
|
+
|
|
1154
|
+
return addTokenToUrl(url);
|
|
1132
1155
|
}
|
|
1133
1156
|
|
|
1134
1157
|
function formatTime(seconds) {
|
|
@@ -1295,7 +1318,7 @@
|
|
|
1295
1318
|
const formData = new FormData();
|
|
1296
1319
|
formData.append('file', file);
|
|
1297
1320
|
|
|
1298
|
-
const response = await fetch(`${API_BASE}/upload-instrumental
|
|
1321
|
+
const response = await fetch(addTokenToUrl(`${API_BASE}/upload-instrumental`), {
|
|
1299
1322
|
method: 'POST',
|
|
1300
1323
|
body: formData
|
|
1301
1324
|
});
|
|
@@ -1354,7 +1377,7 @@
|
|
|
1354
1377
|
}
|
|
1355
1378
|
|
|
1356
1379
|
try {
|
|
1357
|
-
const response = await fetch(`${API_BASE}/create-custom-instrumental
|
|
1380
|
+
const response = await fetch(addTokenToUrl(`${API_BASE}/create-custom-instrumental`), {
|
|
1358
1381
|
method: 'POST',
|
|
1359
1382
|
headers: { 'Content-Type': 'application/json' },
|
|
1360
1383
|
body: JSON.stringify({ mute_regions: muteRegions })
|
|
@@ -1404,7 +1427,7 @@
|
|
|
1404
1427
|
}
|
|
1405
1428
|
|
|
1406
1429
|
try {
|
|
1407
|
-
const response = await fetch(`${API_BASE}/select-instrumental
|
|
1430
|
+
const response = await fetch(addTokenToUrl(`${API_BASE}/select-instrumental`), {
|
|
1408
1431
|
method: 'POST',
|
|
1409
1432
|
headers: { 'Content-Type': 'application/json' },
|
|
1410
1433
|
body: JSON.stringify({ selection: selectedOption })
|
|
@@ -654,7 +654,31 @@ class KaraokeFinalise:
|
|
|
654
654
|
else:
|
|
655
655
|
self.logger.warning(f"Unsupported file extension: {current_ext}")
|
|
656
656
|
|
|
657
|
-
raise Exception(
|
|
657
|
+
raise Exception(
|
|
658
|
+
"No suitable files found for processing.\n"
|
|
659
|
+
"\n"
|
|
660
|
+
"WHAT THIS MEANS:\n"
|
|
661
|
+
"The finalisation step requires a '(With Vocals).mkv' video file, which is created "
|
|
662
|
+
"during the lyrics transcription phase. This file contains the karaoke video with "
|
|
663
|
+
"synchronized lyrics overlay.\n"
|
|
664
|
+
"\n"
|
|
665
|
+
"COMMON CAUSES:\n"
|
|
666
|
+
"1. Transcription provider not configured - No AUDIOSHAKE_API_TOKEN or RUNPOD_API_KEY set\n"
|
|
667
|
+
"2. Transcription failed - Check logs above for API errors or timeout messages\n"
|
|
668
|
+
"3. Invalid API credentials - Verify your API tokens are correct and active\n"
|
|
669
|
+
"4. Network issues - Unable to reach transcription service\n"
|
|
670
|
+
"5. Running in wrong directory - Make sure you're in the track output folder\n"
|
|
671
|
+
"\n"
|
|
672
|
+
"TROUBLESHOOTING STEPS:\n"
|
|
673
|
+
"1. Check environment variables:\n"
|
|
674
|
+
" - AUDIOSHAKE_API_TOKEN (for AudioShake transcription)\n"
|
|
675
|
+
" - RUNPOD_API_KEY + WHISPER_RUNPOD_ID (for Whisper transcription)\n"
|
|
676
|
+
"2. Review the log output above for transcription errors\n"
|
|
677
|
+
"3. Try running with --log_level debug for more detailed output\n"
|
|
678
|
+
"4. If you don't need synchronized lyrics, use --skip-lyrics for instrumental-only karaoke\n"
|
|
679
|
+
"\n"
|
|
680
|
+
"See README.md 'Transcription Providers' and 'Troubleshooting' sections for more details."
|
|
681
|
+
)
|
|
658
682
|
|
|
659
683
|
def choose_instrumental_audio_file(self, base_name):
|
|
660
684
|
self.logger.info(f"Choosing instrumental audio file to use as karaoke audio...")
|
karaoke_gen/karaoke_gen.py
CHANGED
|
@@ -31,6 +31,12 @@ from .video_generator import VideoGenerator
|
|
|
31
31
|
from .video_background_processor import VideoBackgroundProcessor
|
|
32
32
|
from .audio_fetcher import create_audio_fetcher, AudioFetcherError, NoResultsError, UserCancelledError
|
|
33
33
|
|
|
34
|
+
# Import lyrics_transcriber components for post-review countdown and video rendering
|
|
35
|
+
from lyrics_transcriber.output.countdown_processor import CountdownProcessor
|
|
36
|
+
from lyrics_transcriber.output.generator import OutputGenerator
|
|
37
|
+
from lyrics_transcriber.types import CorrectionResult
|
|
38
|
+
from lyrics_transcriber.core.config import OutputConfig as LyricsOutputConfig
|
|
39
|
+
|
|
34
40
|
|
|
35
41
|
class KaraokePrep:
|
|
36
42
|
def __init__(
|
|
@@ -74,7 +80,7 @@ class KaraokePrep:
|
|
|
74
80
|
skip_separation=False,
|
|
75
81
|
# Video Background Configuration
|
|
76
82
|
background_video=None,
|
|
77
|
-
background_video_darkness=
|
|
83
|
+
background_video_darkness=50,
|
|
78
84
|
# Audio Fetcher Configuration
|
|
79
85
|
auto_download=False,
|
|
80
86
|
):
|
|
@@ -482,41 +488,56 @@ class KaraokePrep:
|
|
|
482
488
|
self.logger.info(f"Found existing media files matching extractor '{self.extractor}', skipping download/conversion.")
|
|
483
489
|
|
|
484
490
|
elif getattr(self, '_use_audio_fetcher', False):
|
|
485
|
-
# Use flacfetch to search and download audio
|
|
486
|
-
self.logger.info(f"Using flacfetch to search and download: {self.artist} - {self.title}")
|
|
487
|
-
|
|
488
491
|
try:
|
|
489
|
-
#
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
492
|
+
# Check if this is a URL download or search+download
|
|
493
|
+
if getattr(self, '_use_url_download', False):
|
|
494
|
+
# Direct URL download (e.g., YouTube URL)
|
|
495
|
+
self.logger.info(f"Using flacfetch to download from URL: {self.url}")
|
|
496
|
+
|
|
497
|
+
fetch_result = self.audio_fetcher.download_from_url(
|
|
498
|
+
url=self.url,
|
|
499
|
+
output_dir=track_output_dir,
|
|
500
|
+
output_filename=f"{artist_title} (youtube)" if artist_title != "Unknown - Unknown" else None,
|
|
501
|
+
artist=self.artist,
|
|
502
|
+
title=self.title,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
# Update extractor to reflect the source
|
|
506
|
+
self.extractor = "youtube"
|
|
507
|
+
else:
|
|
508
|
+
# Use flacfetch to search and download audio
|
|
509
|
+
self.logger.info(f"Using flacfetch to search and download: {self.artist} - {self.title}")
|
|
510
|
+
|
|
511
|
+
fetch_result = self.audio_fetcher.search_and_download(
|
|
512
|
+
artist=self.artist,
|
|
513
|
+
title=self.title,
|
|
514
|
+
output_dir=track_output_dir,
|
|
515
|
+
output_filename=f"{artist_title} (flacfetch)",
|
|
516
|
+
auto_select=self.auto_download,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Update extractor to reflect the actual provider used
|
|
520
|
+
self.extractor = f"flacfetch-{fetch_result.provider}"
|
|
521
|
+
|
|
501
522
|
# Set up the output paths
|
|
502
523
|
output_filename_no_extension = os.path.join(track_output_dir, f"{artist_title} ({self.extractor})")
|
|
503
|
-
|
|
524
|
+
|
|
504
525
|
# Copy/move the downloaded file to the expected location
|
|
505
526
|
processed_track["input_media"] = self.file_handler.download_audio_from_fetcher_result(
|
|
506
527
|
fetch_result.filepath, output_filename_no_extension
|
|
507
528
|
)
|
|
508
|
-
|
|
529
|
+
|
|
509
530
|
self.logger.info(f"Audio downloaded from {fetch_result.provider}: {processed_track['input_media']}")
|
|
510
|
-
|
|
531
|
+
|
|
511
532
|
# Convert to WAV for audio processing
|
|
512
533
|
self.logger.info("Converting downloaded audio to WAV for processing...")
|
|
513
534
|
processed_track["input_audio_wav"] = self.file_handler.convert_to_wav(
|
|
514
535
|
processed_track["input_media"], output_filename_no_extension
|
|
515
536
|
)
|
|
516
|
-
|
|
537
|
+
|
|
517
538
|
# No still image for audio-only downloads
|
|
518
539
|
processed_track["input_still_image"] = None
|
|
519
|
-
|
|
540
|
+
|
|
520
541
|
except UserCancelledError:
|
|
521
542
|
# User cancelled - propagate up to CLI for graceful exit
|
|
522
543
|
raise
|
|
@@ -692,6 +713,112 @@ class KaraokePrep:
|
|
|
692
713
|
|
|
693
714
|
self.logger.info("=== Parallel Processing Complete ===")
|
|
694
715
|
|
|
716
|
+
# === POST-TRANSCRIPTION: Add countdown and render video ===
|
|
717
|
+
# Since lyrics_processor.py now always defers countdown and video rendering,
|
|
718
|
+
# we handle it here after human review is complete. This ensures the review UI
|
|
719
|
+
# shows accurate, unshifted timestamps (same behavior as cloud backend).
|
|
720
|
+
if processed_track.get("lyrics") and self.render_video:
|
|
721
|
+
self.logger.info("=== Processing Countdown and Video Rendering ===")
|
|
722
|
+
|
|
723
|
+
from .utils import sanitize_filename
|
|
724
|
+
sanitized_artist = sanitize_filename(self.artist)
|
|
725
|
+
sanitized_title = sanitize_filename(self.title)
|
|
726
|
+
lyrics_dir = os.path.join(track_output_dir, "lyrics")
|
|
727
|
+
|
|
728
|
+
# Find the corrections JSON file
|
|
729
|
+
corrections_filename = f"{sanitized_artist} - {sanitized_title} (Lyrics Corrections).json"
|
|
730
|
+
corrections_filepath = os.path.join(lyrics_dir, corrections_filename)
|
|
731
|
+
|
|
732
|
+
if os.path.exists(corrections_filepath):
|
|
733
|
+
self.logger.info(f"Loading corrections from: {corrections_filepath}")
|
|
734
|
+
|
|
735
|
+
with open(corrections_filepath, 'r', encoding='utf-8') as f:
|
|
736
|
+
corrections_data = json.load(f)
|
|
737
|
+
|
|
738
|
+
# Convert to CorrectionResult
|
|
739
|
+
correction_result = CorrectionResult.from_dict(corrections_data)
|
|
740
|
+
self.logger.info(f"Loaded CorrectionResult with {len(correction_result.corrected_segments)} segments")
|
|
741
|
+
|
|
742
|
+
# Get the audio file path
|
|
743
|
+
audio_path = processed_track["input_audio_wav"]
|
|
744
|
+
|
|
745
|
+
# Add countdown intro if needed (songs that start within 3 seconds)
|
|
746
|
+
self.logger.info("Processing countdown intro (if needed)...")
|
|
747
|
+
cache_dir = os.path.join(track_output_dir, "cache")
|
|
748
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
749
|
+
|
|
750
|
+
countdown_processor = CountdownProcessor(
|
|
751
|
+
cache_dir=cache_dir,
|
|
752
|
+
logger=self.logger,
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
correction_result, audio_path, padding_added, padding_seconds = countdown_processor.process(
|
|
756
|
+
correction_result=correction_result,
|
|
757
|
+
audio_filepath=audio_path,
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
# Update processed_track with countdown info
|
|
761
|
+
processed_track["countdown_padding_added"] = padding_added
|
|
762
|
+
processed_track["countdown_padding_seconds"] = padding_seconds
|
|
763
|
+
if padding_added:
|
|
764
|
+
processed_track["padded_vocals_audio"] = audio_path
|
|
765
|
+
self.logger.info(
|
|
766
|
+
f"=== COUNTDOWN PADDING ADDED ===\n"
|
|
767
|
+
f"Added {padding_seconds}s padding to audio and shifted timestamps.\n"
|
|
768
|
+
f"Instrumental tracks will be padded after separation to maintain sync."
|
|
769
|
+
)
|
|
770
|
+
else:
|
|
771
|
+
self.logger.info("No countdown needed - song starts after 3 seconds")
|
|
772
|
+
|
|
773
|
+
# Save the updated corrections with countdown timestamps
|
|
774
|
+
updated_corrections_data = correction_result.to_dict()
|
|
775
|
+
with open(corrections_filepath, 'w', encoding='utf-8') as f:
|
|
776
|
+
json.dump(updated_corrections_data, f, indent=2)
|
|
777
|
+
self.logger.info(f"Saved countdown-adjusted corrections to: {corrections_filepath}")
|
|
778
|
+
|
|
779
|
+
# Render video with lyrics
|
|
780
|
+
self.logger.info("Rendering karaoke video with synchronized lyrics...")
|
|
781
|
+
|
|
782
|
+
output_config = LyricsOutputConfig(
|
|
783
|
+
output_dir=lyrics_dir,
|
|
784
|
+
cache_dir=cache_dir,
|
|
785
|
+
output_styles_json=self.style_params_json,
|
|
786
|
+
render_video=True,
|
|
787
|
+
generate_cdg=False,
|
|
788
|
+
generate_plain_text=True,
|
|
789
|
+
generate_lrc=True,
|
|
790
|
+
video_resolution="4k",
|
|
791
|
+
subtitle_offset_ms=self.subtitle_offset_ms,
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
output_generator = OutputGenerator(output_config, self.logger)
|
|
795
|
+
output_prefix = f"{sanitized_artist} - {sanitized_title}"
|
|
796
|
+
|
|
797
|
+
outputs = output_generator.generate_outputs(
|
|
798
|
+
transcription_corrected=correction_result,
|
|
799
|
+
audio_filepath=audio_path,
|
|
800
|
+
output_prefix=output_prefix,
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
# Copy video to expected location in parent directory
|
|
804
|
+
if outputs and outputs.get("video_filepath"):
|
|
805
|
+
source_video = outputs["video_filepath"]
|
|
806
|
+
dest_video = os.path.join(track_output_dir, f"{artist_title} (With Vocals).mkv")
|
|
807
|
+
shutil.copy2(source_video, dest_video)
|
|
808
|
+
self.logger.info(f"Video rendered successfully: {dest_video}")
|
|
809
|
+
processed_track["with_vocals_video"] = dest_video
|
|
810
|
+
|
|
811
|
+
# Update ASS filepath for video background processing
|
|
812
|
+
if outputs.get("ass_filepath"):
|
|
813
|
+
processed_track["ass_filepath"] = outputs["ass_filepath"]
|
|
814
|
+
else:
|
|
815
|
+
self.logger.warning("Video rendering did not produce expected output")
|
|
816
|
+
else:
|
|
817
|
+
self.logger.warning(f"Corrections file not found: {corrections_filepath}")
|
|
818
|
+
self.logger.warning("Skipping countdown processing and video rendering")
|
|
819
|
+
elif not self.render_video:
|
|
820
|
+
self.logger.info("Video rendering disabled - skipping countdown and video generation")
|
|
821
|
+
|
|
695
822
|
# Apply video background if requested and lyrics were processed
|
|
696
823
|
if self.video_background_processor and processed_track.get("lyrics"):
|
|
697
824
|
self.logger.info("=== Processing Video Background ===")
|
|
@@ -864,15 +991,21 @@ class KaraokePrep:
|
|
|
864
991
|
|
|
865
992
|
# If separated_audio is empty (e.g., transcription was skipped but existing files have countdown),
|
|
866
993
|
# scan the directory for existing instrumental files
|
|
994
|
+
# Note: also check for Custom instrumental (provided via --existing_instrumental)
|
|
867
995
|
has_instrumentals = (
|
|
868
996
|
processed_track["separated_audio"].get("clean_instrumental", {}).get("instrumental") or
|
|
869
|
-
processed_track["separated_audio"].get("combined_instrumentals")
|
|
997
|
+
processed_track["separated_audio"].get("combined_instrumentals") or
|
|
998
|
+
processed_track["separated_audio"].get("Custom", {}).get("instrumental")
|
|
870
999
|
)
|
|
871
1000
|
if not has_instrumentals:
|
|
872
1001
|
self.logger.info("No instrumentals in separated_audio, scanning directory for existing files...")
|
|
1002
|
+
# Preserve existing Custom key if present before overwriting
|
|
1003
|
+
custom_backup = processed_track["separated_audio"].get("Custom")
|
|
873
1004
|
processed_track["separated_audio"] = self._scan_directory_for_instrumentals(
|
|
874
1005
|
track_output_dir, artist_title
|
|
875
1006
|
)
|
|
1007
|
+
if custom_backup:
|
|
1008
|
+
processed_track["separated_audio"]["Custom"] = custom_backup
|
|
876
1009
|
|
|
877
1010
|
# Apply padding using AudioProcessor
|
|
878
1011
|
padded_separation_result = self.audio_processor.apply_countdown_padding_to_instrumentals(
|
|
@@ -901,11 +1034,11 @@ class KaraokePrep:
|
|
|
901
1034
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
902
1035
|
loop.remove_signal_handler(sig)
|
|
903
1036
|
|
|
904
|
-
async def shutdown(self,
|
|
1037
|
+
async def shutdown(self, signal_received):
|
|
905
1038
|
"""Handle shutdown signals gracefully."""
|
|
906
|
-
self.logger.info(f"Received exit signal {
|
|
1039
|
+
self.logger.info(f"Received exit signal {signal_received.name}...")
|
|
907
1040
|
|
|
908
|
-
# Get all running tasks
|
|
1041
|
+
# Get all running tasks except the current shutdown task
|
|
909
1042
|
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
|
910
1043
|
|
|
911
1044
|
if tasks:
|
|
@@ -914,17 +1047,15 @@ class KaraokePrep:
|
|
|
914
1047
|
for task in tasks:
|
|
915
1048
|
task.cancel()
|
|
916
1049
|
|
|
917
|
-
self.logger.info("Received cancellation request, cleaning up...")
|
|
918
|
-
|
|
919
1050
|
# Wait for all tasks to complete with cancellation
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
except asyncio.CancelledError:
|
|
923
|
-
pass
|
|
1051
|
+
# Use return_exceptions=True to gather all results without raising
|
|
1052
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
924
1053
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1054
|
+
self.logger.info("Cleanup complete")
|
|
1055
|
+
|
|
1056
|
+
# Raise KeyboardInterrupt to propagate the cancellation up the call stack
|
|
1057
|
+
# This allows the main event loop to exit cleanly
|
|
1058
|
+
raise KeyboardInterrupt()
|
|
928
1059
|
|
|
929
1060
|
async def process_playlist(self):
|
|
930
1061
|
if self.artist is None or self.title is None:
|
|
@@ -987,17 +1118,56 @@ class KaraokePrep:
|
|
|
987
1118
|
|
|
988
1119
|
return tracks
|
|
989
1120
|
|
|
1121
|
+
def _is_url(self, string: str) -> bool:
|
|
1122
|
+
"""Check if a string is a URL."""
|
|
1123
|
+
return string is not None and (string.startswith("http://") or string.startswith("https://"))
|
|
1124
|
+
|
|
990
1125
|
async def process(self):
|
|
991
1126
|
if self.input_media is not None and os.path.isdir(self.input_media):
|
|
992
1127
|
self.logger.info(f"Input media {self.input_media} is a local folder, processing each file individually...")
|
|
993
1128
|
return await self.process_folder()
|
|
994
1129
|
elif self.input_media is not None and os.path.isfile(self.input_media):
|
|
995
1130
|
self.logger.info(f"Input media {self.input_media} is a local file, audio download will be skipped")
|
|
1131
|
+
return [await self.prep_single_track()]
|
|
1132
|
+
elif self.input_media is not None and self._is_url(self.input_media):
|
|
1133
|
+
# URL provided - download directly via flacfetch
|
|
1134
|
+
self.logger.info(f"Input media {self.input_media} is a URL, downloading via flacfetch...")
|
|
1135
|
+
|
|
1136
|
+
# Extract video ID for metadata if it's a YouTube URL
|
|
1137
|
+
video_id = None
|
|
1138
|
+
youtube_patterns = [
|
|
1139
|
+
r'(?:youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})',
|
|
1140
|
+
r'youtube\.com/embed/([a-zA-Z0-9_-]{11})',
|
|
1141
|
+
r'youtube\.com/v/([a-zA-Z0-9_-]{11})',
|
|
1142
|
+
]
|
|
1143
|
+
for pattern in youtube_patterns:
|
|
1144
|
+
match = re.search(pattern, self.input_media)
|
|
1145
|
+
if match:
|
|
1146
|
+
video_id = match.group(1)
|
|
1147
|
+
break
|
|
1148
|
+
|
|
1149
|
+
# Set up the extracted_info for metadata consistency
|
|
1150
|
+
self.extracted_info = {
|
|
1151
|
+
"title": f"{self.artist} - {self.title}" if self.artist and self.title else video_id or "Unknown",
|
|
1152
|
+
"artist": self.artist or "",
|
|
1153
|
+
"track_title": self.title or "",
|
|
1154
|
+
"extractor_key": "youtube",
|
|
1155
|
+
"id": video_id or self.input_media,
|
|
1156
|
+
"url": self.input_media,
|
|
1157
|
+
"source": "youtube",
|
|
1158
|
+
}
|
|
1159
|
+
self.extractor = "youtube"
|
|
1160
|
+
self.url = self.input_media
|
|
1161
|
+
|
|
1162
|
+
# Mark that we need to use audio fetcher for URL download
|
|
1163
|
+
self._use_audio_fetcher = True
|
|
1164
|
+
self._use_url_download = True # New flag for URL-based download
|
|
1165
|
+
|
|
996
1166
|
return [await self.prep_single_track()]
|
|
997
1167
|
elif self.artist and self.title:
|
|
998
1168
|
# No input file provided - use flacfetch to search and download audio
|
|
999
1169
|
self.logger.info(f"No input file provided, using flacfetch to search for: {self.artist} - {self.title}")
|
|
1000
|
-
|
|
1170
|
+
|
|
1001
1171
|
# Set up the extracted_info for metadata consistency
|
|
1002
1172
|
self.extracted_info = {
|
|
1003
1173
|
"title": f"{self.artist} - {self.title}",
|
|
@@ -1010,13 +1180,12 @@ class KaraokePrep:
|
|
|
1010
1180
|
}
|
|
1011
1181
|
self.extractor = "flacfetch"
|
|
1012
1182
|
self.url = None # URL will be determined by flacfetch
|
|
1013
|
-
|
|
1183
|
+
|
|
1014
1184
|
# Mark that we need to use audio fetcher for download
|
|
1015
1185
|
self._use_audio_fetcher = True
|
|
1016
|
-
|
|
1186
|
+
|
|
1017
1187
|
return [await self.prep_single_track()]
|
|
1018
1188
|
else:
|
|
1019
1189
|
raise ValueError(
|
|
1020
|
-
"Either a local file path or both artist and title must be provided.
|
|
1021
|
-
"URL-based input has been replaced with flacfetch audio fetching."
|
|
1190
|
+
"Either a local file path, a URL, or both artist and title must be provided."
|
|
1022
1191
|
)
|