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,424 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
import importlib.resources as pkg_resources
|
|
4
|
+
import shutil
|
|
5
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Placeholder class or functions for video/image generation
|
|
9
|
+
class VideoGenerator:
|
|
10
|
+
def __init__(self, logger, ffmpeg_base_command, render_bounding_boxes, output_png, output_jpg):
|
|
11
|
+
self.logger = logger
|
|
12
|
+
self.ffmpeg_base_command = ffmpeg_base_command
|
|
13
|
+
self.render_bounding_boxes = render_bounding_boxes
|
|
14
|
+
self.output_png = output_png
|
|
15
|
+
self.output_jpg = output_jpg
|
|
16
|
+
|
|
17
|
+
def parse_region(self, region_str):
|
|
18
|
+
if region_str:
|
|
19
|
+
try:
|
|
20
|
+
parts = region_str.split(",")
|
|
21
|
+
if len(parts) != 4:
|
|
22
|
+
raise ValueError(f"Invalid region format: {region_str}. Expected 4 elements: 'x,y,width,height'")
|
|
23
|
+
return tuple(map(int, parts))
|
|
24
|
+
except ValueError as e:
|
|
25
|
+
# Re-raise specific format errors or general ValueError for int conversion issues
|
|
26
|
+
if "Expected 4 elements" in str(e):
|
|
27
|
+
raise e
|
|
28
|
+
raise ValueError(f"Invalid region format: {region_str}. Could not convert to integers. Expected format: 'x,y,width,height'") from e
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
def hex_to_rgb(self, hex_color):
|
|
32
|
+
"""Convert hex color to RGB tuple."""
|
|
33
|
+
hex_color = hex_color.lstrip("#")
|
|
34
|
+
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
|
|
35
|
+
|
|
36
|
+
# Placeholder methods - to be filled by user moving code
|
|
37
|
+
def create_video(
|
|
38
|
+
self,
|
|
39
|
+
extra_text,
|
|
40
|
+
title_text,
|
|
41
|
+
artist_text,
|
|
42
|
+
format,
|
|
43
|
+
output_image_filepath_noext,
|
|
44
|
+
output_video_filepath,
|
|
45
|
+
existing_image=None,
|
|
46
|
+
duration=5,
|
|
47
|
+
):
|
|
48
|
+
"""Create a video with title, artist, and optional extra text."""
|
|
49
|
+
self.logger.debug(f"Creating video with extra_text: '{extra_text}'")
|
|
50
|
+
self.logger.debug(f"Format settings: {format}")
|
|
51
|
+
|
|
52
|
+
resolution = (3840, 2160) # 4K resolution
|
|
53
|
+
self.logger.info(f"Creating video with format: {format}")
|
|
54
|
+
self.logger.info(f"extra_text: {extra_text}, artist_text: {artist_text}, title_text: {title_text}")
|
|
55
|
+
|
|
56
|
+
if existing_image:
|
|
57
|
+
return self._handle_existing_image(existing_image, output_image_filepath_noext, output_video_filepath, duration)
|
|
58
|
+
|
|
59
|
+
# Create or load background
|
|
60
|
+
background = self._create_background(format, resolution)
|
|
61
|
+
draw = ImageDraw.Draw(background)
|
|
62
|
+
|
|
63
|
+
if format["font"] is not None:
|
|
64
|
+
self.logger.info(f"Using font: {format['font']}")
|
|
65
|
+
# Check if the font path is absolute
|
|
66
|
+
if os.path.isabs(format["font"]):
|
|
67
|
+
font_path = format["font"]
|
|
68
|
+
if not os.path.exists(font_path):
|
|
69
|
+
self.logger.warning(f"Font file not found at {font_path}, falling back to default font")
|
|
70
|
+
font_path = None
|
|
71
|
+
else:
|
|
72
|
+
# Try to load from package resources
|
|
73
|
+
try:
|
|
74
|
+
with pkg_resources.path("karaoke_gen.resources", format["font"]) as font_path:
|
|
75
|
+
font_path = str(font_path)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
self.logger.warning(f"Could not load font from resources: {e}, falling back to default font")
|
|
78
|
+
font_path = None
|
|
79
|
+
|
|
80
|
+
# Render all text elements
|
|
81
|
+
self._render_all_text(
|
|
82
|
+
draw,
|
|
83
|
+
font_path,
|
|
84
|
+
title_text,
|
|
85
|
+
artist_text,
|
|
86
|
+
format,
|
|
87
|
+
self.render_bounding_boxes,
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
self.logger.info("No font specified, skipping text rendering")
|
|
91
|
+
|
|
92
|
+
# Save images and create video
|
|
93
|
+
self._save_output_files(
|
|
94
|
+
background, output_image_filepath_noext, output_video_filepath, duration, resolution
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def calculate_text_size_to_fit(self, draw, text, font_path, region):
|
|
98
|
+
font_size = 500 # Start with a large font size
|
|
99
|
+
font = ImageFont.truetype(font_path, size=font_size) if font_path and os.path.exists(font_path) else ImageFont.load_default()
|
|
100
|
+
|
|
101
|
+
def get_text_size(text, font):
|
|
102
|
+
bbox = draw.textbbox((0, 0), text, font=font)
|
|
103
|
+
# Use the actual text height without the font's internal padding
|
|
104
|
+
return bbox[2], bbox[3] - bbox[1]
|
|
105
|
+
|
|
106
|
+
text_width, text_height = get_text_size(text, font)
|
|
107
|
+
target_height = region[3] # Use full region height as target
|
|
108
|
+
|
|
109
|
+
while text_width > region[2] or text_height > target_height:
|
|
110
|
+
font_size -= 10
|
|
111
|
+
if font_size <= 150:
|
|
112
|
+
# Split the text into two lines
|
|
113
|
+
words = text.split()
|
|
114
|
+
mid = len(words) // 2
|
|
115
|
+
line1 = " ".join(words[:mid])
|
|
116
|
+
line2 = " ".join(words[mid:])
|
|
117
|
+
|
|
118
|
+
# Reset font size for two-line layout
|
|
119
|
+
font_size = 500
|
|
120
|
+
font = ImageFont.truetype(font_path, size=font_size) if font_path and os.path.exists(font_path) else ImageFont.load_default()
|
|
121
|
+
|
|
122
|
+
while True:
|
|
123
|
+
text_width1, text_height1 = get_text_size(line1, font)
|
|
124
|
+
text_width2, text_height2 = get_text_size(line2, font)
|
|
125
|
+
total_height = text_height1 + text_height2
|
|
126
|
+
|
|
127
|
+
# Add a small gap between lines (10% of line height)
|
|
128
|
+
line_gap = text_height1 * 0.1
|
|
129
|
+
total_height_with_gap = total_height + line_gap
|
|
130
|
+
|
|
131
|
+
if max(text_width1, text_width2) <= region[2] and total_height_with_gap <= target_height:
|
|
132
|
+
return font, (line1, line2)
|
|
133
|
+
|
|
134
|
+
font_size -= 10
|
|
135
|
+
if font_size <= 0:
|
|
136
|
+
raise ValueError("Cannot fit text within the defined region.")
|
|
137
|
+
font = ImageFont.truetype(font_path, size=font_size) if font_path and os.path.exists(font_path) else ImageFont.load_default()
|
|
138
|
+
|
|
139
|
+
font = ImageFont.truetype(font_path, size=font_size) if font_path and os.path.exists(font_path) else ImageFont.load_default()
|
|
140
|
+
text_width, text_height = get_text_size(text, font)
|
|
141
|
+
|
|
142
|
+
return font, text
|
|
143
|
+
|
|
144
|
+
def _render_text_in_region(self, draw, text, font_path, region, color, gradient=None, font=None):
|
|
145
|
+
"""Helper method to render text within a specified region."""
|
|
146
|
+
self.logger.debug(f"Rendering text: '{text}' in region: {region} with color: {color} gradient: {gradient}")
|
|
147
|
+
|
|
148
|
+
if text is None:
|
|
149
|
+
self.logger.debug("Text is None, skipping rendering")
|
|
150
|
+
return region
|
|
151
|
+
|
|
152
|
+
if region is None:
|
|
153
|
+
self.logger.debug("Region is None, skipping rendering")
|
|
154
|
+
return region
|
|
155
|
+
|
|
156
|
+
if font is None:
|
|
157
|
+
font, text_lines = self.calculate_text_size_to_fit(draw, text, font_path, region)
|
|
158
|
+
else:
|
|
159
|
+
text_lines = text
|
|
160
|
+
|
|
161
|
+
self.logger.debug(f"Using text_lines: {text_lines}")
|
|
162
|
+
|
|
163
|
+
x, y, width, height = region
|
|
164
|
+
|
|
165
|
+
# Get font metrics
|
|
166
|
+
ascent, descent = font.getmetrics()
|
|
167
|
+
font_height = ascent + descent
|
|
168
|
+
|
|
169
|
+
def render_text_with_gradient(text, position, bbox):
|
|
170
|
+
# Convert position coordinates to integers
|
|
171
|
+
position = (int(position[0]), int(position[1]))
|
|
172
|
+
|
|
173
|
+
if gradient is None:
|
|
174
|
+
draw.text(position, text, fill=color, font=font)
|
|
175
|
+
else:
|
|
176
|
+
# Create a temporary image for this text
|
|
177
|
+
text_layer = Image.new("RGBA", (bbox[2], bbox[3]), (0, 0, 0, 0))
|
|
178
|
+
text_draw = ImageDraw.Draw(text_layer)
|
|
179
|
+
|
|
180
|
+
# Draw text in first color
|
|
181
|
+
text_draw.text((0, 0), text, fill=gradient["color1"], font=font)
|
|
182
|
+
|
|
183
|
+
# Create and apply gradient mask
|
|
184
|
+
mask = self._create_gradient_mask((bbox[2], bbox[3]), gradient)
|
|
185
|
+
|
|
186
|
+
# Create second color layer
|
|
187
|
+
color2_layer = Image.new("RGBA", (bbox[2], bbox[3]), (0, 0, 0, 0))
|
|
188
|
+
color2_draw = ImageDraw.Draw(color2_layer)
|
|
189
|
+
color2_draw.text((0, 0), text, fill=gradient["color2"], font=font)
|
|
190
|
+
|
|
191
|
+
# Composite using gradient mask
|
|
192
|
+
text_layer.paste(color2_layer, mask=mask)
|
|
193
|
+
|
|
194
|
+
# Paste onto main image
|
|
195
|
+
draw._image.paste(text_layer, position, text_layer)
|
|
196
|
+
|
|
197
|
+
if isinstance(text_lines, tuple): # Two lines
|
|
198
|
+
line1, line2 = text_lines
|
|
199
|
+
bbox1 = draw.textbbox((0, 0), line1, font=font)
|
|
200
|
+
bbox2 = draw.textbbox((0, 0), line2, font=font)
|
|
201
|
+
|
|
202
|
+
# Calculate line heights using bounding boxes
|
|
203
|
+
line1_height = bbox1[3] - bbox1[1]
|
|
204
|
+
line2_height = bbox2[3] - bbox2[1]
|
|
205
|
+
|
|
206
|
+
# Use a small gap between lines (20% of average line height)
|
|
207
|
+
line_gap = int((line1_height + line2_height) * 0.1)
|
|
208
|
+
|
|
209
|
+
# Calculate total height needed
|
|
210
|
+
total_height = line1_height + line_gap + line2_height
|
|
211
|
+
|
|
212
|
+
# Center the entire text block vertically in the region
|
|
213
|
+
y_start = y + (height - total_height) // 2
|
|
214
|
+
|
|
215
|
+
# Draw first line
|
|
216
|
+
pos1 = (x + (width - bbox1[2]) // 2, y_start)
|
|
217
|
+
render_text_with_gradient(line1, pos1, bbox1)
|
|
218
|
+
|
|
219
|
+
# Draw second line
|
|
220
|
+
pos2 = (x + (width - bbox2[2]) // 2, y_start + line1_height + line_gap)
|
|
221
|
+
render_text_with_gradient(line2, pos2, bbox2)
|
|
222
|
+
else:
|
|
223
|
+
# Single line
|
|
224
|
+
bbox = draw.textbbox((0, 0), text_lines, font=font)
|
|
225
|
+
|
|
226
|
+
# Center text vertically using font metrics
|
|
227
|
+
y_pos = y + (height - font_height) // 2
|
|
228
|
+
|
|
229
|
+
position = (x + (width - bbox[2]) // 2, y_pos)
|
|
230
|
+
render_text_with_gradient(text_lines, position, bbox)
|
|
231
|
+
|
|
232
|
+
return region
|
|
233
|
+
|
|
234
|
+
def _draw_bounding_box(self, draw, region, color):
|
|
235
|
+
"""Helper method to draw a bounding box around a region."""
|
|
236
|
+
if region is None:
|
|
237
|
+
self.logger.debug("Region is None, skipping drawing bounding box")
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
x, y, width, height = region
|
|
241
|
+
draw.rectangle([x, y, x + width, y + height], outline=color, width=2)
|
|
242
|
+
|
|
243
|
+
def _create_gradient_mask(self, size, gradient_config):
|
|
244
|
+
"""Create a gradient mask for text coloring.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
size (tuple): (width, height) of the mask
|
|
248
|
+
gradient_config (dict): Configuration with keys:
|
|
249
|
+
- color1: First color (hex)
|
|
250
|
+
- color2: Second color (hex)
|
|
251
|
+
- direction: 'horizontal' or 'vertical'
|
|
252
|
+
- start: Start point of gradient transition (0-1)
|
|
253
|
+
- stop: Stop point of gradient transition (0-1)
|
|
254
|
+
"""
|
|
255
|
+
mask = Image.new("L", size)
|
|
256
|
+
draw = ImageDraw.Draw(mask)
|
|
257
|
+
|
|
258
|
+
width, height = size
|
|
259
|
+
start = gradient_config["start"]
|
|
260
|
+
stop = gradient_config["stop"]
|
|
261
|
+
|
|
262
|
+
if gradient_config["direction"] == "horizontal":
|
|
263
|
+
for x in range(width):
|
|
264
|
+
# Calculate position in gradient (0 to 1)
|
|
265
|
+
pos = x / width
|
|
266
|
+
|
|
267
|
+
# Calculate color intensity
|
|
268
|
+
if pos < start:
|
|
269
|
+
intensity = 0
|
|
270
|
+
elif pos > stop:
|
|
271
|
+
intensity = 255
|
|
272
|
+
else:
|
|
273
|
+
# Linear interpolation between start and stop
|
|
274
|
+
intensity = int(255 * (pos - start) / (stop - start))
|
|
275
|
+
|
|
276
|
+
draw.line([(x, 0), (x, height)], fill=intensity)
|
|
277
|
+
else: # vertical
|
|
278
|
+
for y in range(height):
|
|
279
|
+
pos = y / height
|
|
280
|
+
if pos < start:
|
|
281
|
+
intensity = 0
|
|
282
|
+
elif pos > stop:
|
|
283
|
+
intensity = 255
|
|
284
|
+
else:
|
|
285
|
+
intensity = int(255 * (pos - start) / (stop - start))
|
|
286
|
+
|
|
287
|
+
draw.line([(0, y), (width, y)], fill=intensity)
|
|
288
|
+
|
|
289
|
+
return mask
|
|
290
|
+
|
|
291
|
+
def _handle_existing_image(self, existing_image, output_image_filepath_noext, output_video_filepath, duration):
|
|
292
|
+
"""Handle case where an existing image is provided."""
|
|
293
|
+
self.logger.info(f"Using existing image file: {existing_image}")
|
|
294
|
+
existing_extension = os.path.splitext(existing_image)[1]
|
|
295
|
+
|
|
296
|
+
if existing_extension == ".png":
|
|
297
|
+
self.logger.info(f"Copying existing PNG image file: {existing_image}")
|
|
298
|
+
shutil.copy2(existing_image, output_image_filepath_noext + existing_extension)
|
|
299
|
+
else:
|
|
300
|
+
self.logger.info(f"Converting existing image to PNG")
|
|
301
|
+
existing_image_obj = Image.open(existing_image)
|
|
302
|
+
existing_image_obj.save(output_image_filepath_noext + ".png")
|
|
303
|
+
|
|
304
|
+
if existing_extension != ".jpg":
|
|
305
|
+
self.logger.info(f"Converting existing image to JPG")
|
|
306
|
+
existing_image_obj = Image.open(existing_image)
|
|
307
|
+
if existing_image_obj.mode == "RGBA":
|
|
308
|
+
existing_image_obj = existing_image_obj.convert("RGB")
|
|
309
|
+
existing_image_obj.save(output_image_filepath_noext + ".jpg", quality=95)
|
|
310
|
+
|
|
311
|
+
if duration > 0:
|
|
312
|
+
self._create_video_from_image(output_image_filepath_noext + ".png", output_video_filepath, duration)
|
|
313
|
+
|
|
314
|
+
def _create_background(self, format, resolution):
|
|
315
|
+
"""Create or load the background image."""
|
|
316
|
+
if format["background_image"] and os.path.exists(format["background_image"]):
|
|
317
|
+
self.logger.info(f"Using background image file: {format['background_image']}")
|
|
318
|
+
background = Image.open(format["background_image"])
|
|
319
|
+
else:
|
|
320
|
+
self.logger.info(f"Using background color: {format['background_color']}")
|
|
321
|
+
background = Image.new("RGB", resolution, color=self.hex_to_rgb(format["background_color"]))
|
|
322
|
+
|
|
323
|
+
return background.resize(resolution)
|
|
324
|
+
|
|
325
|
+
def _render_all_text(self, draw, font_path, title_text, artist_text, format, render_bounding_boxes):
|
|
326
|
+
"""Render all text elements on the image."""
|
|
327
|
+
# Render title
|
|
328
|
+
if format["title_region"]:
|
|
329
|
+
region_parsed = self.parse_region(format["title_region"])
|
|
330
|
+
region = self._render_text_in_region(
|
|
331
|
+
draw, title_text, font_path, region_parsed, format["title_color"], gradient=format.get("title_gradient")
|
|
332
|
+
)
|
|
333
|
+
if render_bounding_boxes:
|
|
334
|
+
self._draw_bounding_box(draw, region, format["title_color"])
|
|
335
|
+
|
|
336
|
+
# Render artist
|
|
337
|
+
if format["artist_region"]:
|
|
338
|
+
region_parsed = self.parse_region(format["artist_region"])
|
|
339
|
+
region = self._render_text_in_region(
|
|
340
|
+
draw, artist_text, font_path, region_parsed, format["artist_color"], gradient=format.get("artist_gradient")
|
|
341
|
+
)
|
|
342
|
+
if render_bounding_boxes:
|
|
343
|
+
self._draw_bounding_box(draw, region, format["artist_color"])
|
|
344
|
+
|
|
345
|
+
# Render extra text if provided
|
|
346
|
+
if format["extra_text"]:
|
|
347
|
+
region_parsed = self.parse_region(format["extra_text_region"])
|
|
348
|
+
region = self._render_text_in_region(
|
|
349
|
+
draw, format["extra_text"], font_path, region_parsed, format["extra_text_color"], gradient=format.get("extra_text_gradient")
|
|
350
|
+
)
|
|
351
|
+
if render_bounding_boxes:
|
|
352
|
+
self._draw_bounding_box(draw, region, format["extra_text_color"])
|
|
353
|
+
|
|
354
|
+
def _save_output_files(
|
|
355
|
+
self, background, output_image_filepath_noext, output_video_filepath, duration, resolution
|
|
356
|
+
):
|
|
357
|
+
"""Save the output image files and create video if needed."""
|
|
358
|
+
# Save static background image
|
|
359
|
+
if self.output_png:
|
|
360
|
+
background.save(f"{output_image_filepath_noext}.png")
|
|
361
|
+
|
|
362
|
+
if self.output_jpg:
|
|
363
|
+
# Save static background image as JPG for smaller filesize
|
|
364
|
+
background_rgb = background.convert("RGB")
|
|
365
|
+
background_rgb.save(f"{output_image_filepath_noext}.jpg", quality=95)
|
|
366
|
+
|
|
367
|
+
if duration > 0:
|
|
368
|
+
self._create_video_from_image(f"{output_image_filepath_noext}.png", output_video_filepath, duration, resolution)
|
|
369
|
+
|
|
370
|
+
def _create_video_from_image(self, image_path, video_path, duration, resolution=(3840, 2160)):
|
|
371
|
+
"""Create a video from a static image."""
|
|
372
|
+
ffmpeg_command = (
|
|
373
|
+
f'{self.ffmpeg_base_command} -y -loop 1 -framerate 30 -i "{image_path}" '
|
|
374
|
+
f"-f lavfi -i anullsrc -c:v libx264 -r 30 -t {duration} -pix_fmt yuv420p "
|
|
375
|
+
f'-vf scale={resolution[0]}:{resolution[1]} -c:a aac -shortest "{video_path}"'
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
self.logger.info("Generating video...")
|
|
379
|
+
self.logger.debug(f"Running command: {ffmpeg_command}")
|
|
380
|
+
os.system(ffmpeg_command)
|
|
381
|
+
|
|
382
|
+
def _transform_text(self, text, transform_type):
|
|
383
|
+
"""Helper method to transform text based on specified type."""
|
|
384
|
+
if text is None:
|
|
385
|
+
return None # Return None if input is None
|
|
386
|
+
if transform_type == "uppercase":
|
|
387
|
+
return text.upper()
|
|
388
|
+
elif transform_type == "lowercase":
|
|
389
|
+
return text.lower()
|
|
390
|
+
elif transform_type == "propercase":
|
|
391
|
+
return text.title()
|
|
392
|
+
return text # "none" or any other value returns original text
|
|
393
|
+
|
|
394
|
+
def create_title_video(
|
|
395
|
+
self, artist, title, format, output_image_filepath_noext, output_video_filepath, existing_title_image, intro_video_duration
|
|
396
|
+
):
|
|
397
|
+
title_text = self._transform_text(title, format["title_text_transform"])
|
|
398
|
+
artist_text = self._transform_text(artist, format["artist_text_transform"])
|
|
399
|
+
self.create_video(
|
|
400
|
+
title_text=title_text,
|
|
401
|
+
artist_text=artist_text,
|
|
402
|
+
extra_text=format["extra_text"],
|
|
403
|
+
format=format,
|
|
404
|
+
output_image_filepath_noext=output_image_filepath_noext,
|
|
405
|
+
output_video_filepath=output_video_filepath,
|
|
406
|
+
existing_image=existing_title_image,
|
|
407
|
+
duration=intro_video_duration,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
def create_end_video(
|
|
411
|
+
self, artist, title, format, output_image_filepath_noext, output_video_filepath, existing_end_image, end_video_duration
|
|
412
|
+
):
|
|
413
|
+
title_text = self._transform_text(title, format["title_text_transform"])
|
|
414
|
+
artist_text = self._transform_text(artist, format["artist_text_transform"])
|
|
415
|
+
self.create_video(
|
|
416
|
+
title_text=title_text,
|
|
417
|
+
artist_text=artist_text,
|
|
418
|
+
extra_text=format["extra_text"],
|
|
419
|
+
format=format,
|
|
420
|
+
output_image_filepath_noext=output_image_filepath_noext,
|
|
421
|
+
output_video_filepath=output_video_filepath,
|
|
422
|
+
existing_image=existing_end_image,
|
|
423
|
+
duration=end_video_duration,
|
|
424
|
+
)
|