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,180 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ScreenConfig:
|
|
5
|
+
"""Configuration for screen timing and layout.
|
|
6
|
+
|
|
7
|
+
Lead-in Indicator Configuration:
|
|
8
|
+
lead_in_enabled: bool - Enable/disable the lead-in indicator entirely (default: True)
|
|
9
|
+
lead_in_width_percent: float - Width as percentage of screen width (default: 3.5)
|
|
10
|
+
lead_in_height_percent: float - Height as percentage of screen height (default: 4.0)
|
|
11
|
+
lead_in_opacity_percent: float - Opacity percentage, 0-100 (default: 70.0)
|
|
12
|
+
lead_in_outline_thickness: int - Outline thickness in pixels, 0 for no outline (default: 0)
|
|
13
|
+
lead_in_outline_color: str - Outline color in RGB format "R, G, B" (default: "0, 0, 0")
|
|
14
|
+
lead_in_gap_threshold: float - Minimum gap in seconds to show lead-in (default: 5.0)
|
|
15
|
+
lead_in_color: str - Fill color in RGB format "R, G, B" (default: "112, 112, 247")
|
|
16
|
+
lead_in_horiz_offset_percent: float - Horizontal offset as percentage of screen width, can be negative (default: 0.0)
|
|
17
|
+
lead_in_vert_offset_percent: float - Vertical offset as percentage of screen height, can be negative (default: 0.0)
|
|
18
|
+
|
|
19
|
+
Example JSON configuration:
|
|
20
|
+
{
|
|
21
|
+
"karaoke": {
|
|
22
|
+
"lead_in_enabled": true,
|
|
23
|
+
"lead_in_width_percent": 4.0,
|
|
24
|
+
"lead_in_height_percent": 5.0,
|
|
25
|
+
"lead_in_opacity_percent": 80,
|
|
26
|
+
"lead_in_outline_thickness": 2,
|
|
27
|
+
"lead_in_outline_color": "255, 255, 255",
|
|
28
|
+
"lead_in_gap_threshold": 3.0,
|
|
29
|
+
"lead_in_color": "230, 139, 33",
|
|
30
|
+
"lead_in_horiz_offset_percent": -2.0,
|
|
31
|
+
"lead_in_vert_offset_percent": 1.0
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
line_height: int = 50,
|
|
39
|
+
max_visible_lines: int = 4,
|
|
40
|
+
top_padding: int = None,
|
|
41
|
+
video_width: int = 640,
|
|
42
|
+
video_height: int = 360,
|
|
43
|
+
screen_gap_threshold: float = 5.0,
|
|
44
|
+
post_roll_time: float = 1.0,
|
|
45
|
+
fade_in_ms: int = 200,
|
|
46
|
+
fade_out_ms: int = 300,
|
|
47
|
+
lead_in_color: str = "112, 112, 247", # Default blue color in RGB format
|
|
48
|
+
text_case_transform: str = "none", # Options: "none", "uppercase", "lowercase", "propercase"
|
|
49
|
+
# New lead-in indicator configuration options
|
|
50
|
+
lead_in_enabled: bool = True,
|
|
51
|
+
lead_in_width_percent: float = 3.5,
|
|
52
|
+
lead_in_height_percent: float = 4.0,
|
|
53
|
+
lead_in_opacity_percent: float = 70.0,
|
|
54
|
+
lead_in_outline_thickness: int = 0,
|
|
55
|
+
lead_in_outline_color: str = "0, 0, 0",
|
|
56
|
+
lead_in_gap_threshold: float = 5.0,
|
|
57
|
+
lead_in_horiz_offset_percent: float = 0.0,
|
|
58
|
+
lead_in_vert_offset_percent: float = 0.0,
|
|
59
|
+
):
|
|
60
|
+
# Screen layout
|
|
61
|
+
self.max_visible_lines = max_visible_lines
|
|
62
|
+
self.line_height = line_height
|
|
63
|
+
self.top_padding = top_padding if top_padding is not None else line_height
|
|
64
|
+
self.video_height = video_height
|
|
65
|
+
self.video_width = video_width
|
|
66
|
+
# Timing configuration
|
|
67
|
+
self.screen_gap_threshold = screen_gap_threshold
|
|
68
|
+
self.post_roll_time = post_roll_time
|
|
69
|
+
self.fade_in_ms = fade_in_ms
|
|
70
|
+
self.fade_out_ms = fade_out_ms
|
|
71
|
+
# Lead-in configuration
|
|
72
|
+
self.lead_in_color = lead_in_color
|
|
73
|
+
self.lead_in_enabled = lead_in_enabled
|
|
74
|
+
self.lead_in_width_percent = lead_in_width_percent
|
|
75
|
+
self.lead_in_height_percent = lead_in_height_percent
|
|
76
|
+
self.lead_in_opacity_percent = lead_in_opacity_percent
|
|
77
|
+
self.lead_in_outline_thickness = lead_in_outline_thickness
|
|
78
|
+
self.lead_in_outline_color = lead_in_outline_color
|
|
79
|
+
self.lead_in_gap_threshold = lead_in_gap_threshold
|
|
80
|
+
self.lead_in_horiz_offset_percent = lead_in_horiz_offset_percent
|
|
81
|
+
self.lead_in_vert_offset_percent = lead_in_vert_offset_percent
|
|
82
|
+
# Text formatting configuration
|
|
83
|
+
self.text_case_transform = text_case_transform
|
|
84
|
+
|
|
85
|
+
def get_lead_in_color_ass_format(self) -> str:
|
|
86
|
+
"""Convert RGB lead-in color to ASS format.
|
|
87
|
+
|
|
88
|
+
Accepts either:
|
|
89
|
+
- RGB format: "112, 112, 247"
|
|
90
|
+
- ASS format: "&HF77070&" (for backward compatibility)
|
|
91
|
+
|
|
92
|
+
Returns ASS format color string.
|
|
93
|
+
"""
|
|
94
|
+
color_str = self.lead_in_color.strip()
|
|
95
|
+
|
|
96
|
+
# If already in ASS format, return as-is
|
|
97
|
+
if color_str.startswith("&H") and color_str.endswith("&"):
|
|
98
|
+
return color_str
|
|
99
|
+
|
|
100
|
+
# Parse RGB format "R, G, B" or "R, G, B, A"
|
|
101
|
+
try:
|
|
102
|
+
parts = [int(x.strip()) for x in color_str.split(",")]
|
|
103
|
+
if len(parts) == 3:
|
|
104
|
+
r, g, b = parts
|
|
105
|
+
a = 255 # Default full opacity
|
|
106
|
+
elif len(parts) == 4:
|
|
107
|
+
r, g, b, a = parts
|
|
108
|
+
else:
|
|
109
|
+
raise ValueError(f"Invalid color format: {color_str}")
|
|
110
|
+
|
|
111
|
+
# Convert to ASS format: &H{alpha}{blue}{green}{red}&
|
|
112
|
+
# Note: alpha is inverted in ASS (255-a)
|
|
113
|
+
return f"&H{255-a:02X}{b:02X}{g:02X}{r:02X}&"
|
|
114
|
+
|
|
115
|
+
except (ValueError, TypeError) as e:
|
|
116
|
+
# Fallback to default blue if parsing fails
|
|
117
|
+
return "&HF77070&"
|
|
118
|
+
|
|
119
|
+
def get_lead_in_outline_color_ass_format(self) -> str:
|
|
120
|
+
"""Convert RGB lead-in outline color to ASS format.
|
|
121
|
+
|
|
122
|
+
Accepts either:
|
|
123
|
+
- RGB format: "0, 0, 0"
|
|
124
|
+
- ASS format: "&H000000&" (for backward compatibility)
|
|
125
|
+
|
|
126
|
+
Returns ASS format color string.
|
|
127
|
+
"""
|
|
128
|
+
color_str = self.lead_in_outline_color.strip()
|
|
129
|
+
|
|
130
|
+
# If already in ASS format, return as-is
|
|
131
|
+
if color_str.startswith("&H") and color_str.endswith("&"):
|
|
132
|
+
return color_str
|
|
133
|
+
|
|
134
|
+
# Parse RGB format "R, G, B" or "R, G, B, A"
|
|
135
|
+
try:
|
|
136
|
+
parts = [int(x.strip()) for x in color_str.split(",")]
|
|
137
|
+
if len(parts) == 3:
|
|
138
|
+
r, g, b = parts
|
|
139
|
+
a = 255 # Default full opacity
|
|
140
|
+
elif len(parts) == 4:
|
|
141
|
+
r, g, b, a = parts
|
|
142
|
+
else:
|
|
143
|
+
raise ValueError(f"Invalid color format: {color_str}")
|
|
144
|
+
|
|
145
|
+
# Convert to ASS format: &H{alpha}{blue}{green}{red}&
|
|
146
|
+
# Note: alpha is inverted in ASS (255-a)
|
|
147
|
+
return f"&H{255-a:02X}{b:02X}{g:02X}{r:02X}&"
|
|
148
|
+
|
|
149
|
+
except (ValueError, TypeError) as e:
|
|
150
|
+
# Fallback to default black if parsing fails
|
|
151
|
+
return "&H000000&"
|
|
152
|
+
|
|
153
|
+
def get_lead_in_opacity_ass_format(self) -> str:
|
|
154
|
+
"""Convert opacity percentage to ASS alpha format.
|
|
155
|
+
|
|
156
|
+
Returns ASS alpha value (e.g., &H4D& for 70% opacity).
|
|
157
|
+
"""
|
|
158
|
+
# ASS alpha is inverted: 0=opaque, 255=transparent
|
|
159
|
+
# Convert percentage to alpha value
|
|
160
|
+
alpha = int((100 - self.lead_in_opacity_percent) / 100 * 255)
|
|
161
|
+
return f"&H{alpha:02X}&"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class LineTimingInfo:
|
|
166
|
+
"""Timing information for a single line."""
|
|
167
|
+
|
|
168
|
+
fade_in_time: float
|
|
169
|
+
end_time: float
|
|
170
|
+
fade_out_time: float
|
|
171
|
+
clear_time: float
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class LineState:
|
|
176
|
+
"""Complete state for a single line."""
|
|
177
|
+
|
|
178
|
+
text: str
|
|
179
|
+
timing: LineTimingInfo
|
|
180
|
+
y_position: int
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Alignment constants
|
|
2
|
+
ALIGN_BOTTOM_LEFT = 1
|
|
3
|
+
ALIGN_BOTTOM_CENTER = 2
|
|
4
|
+
ALIGN_BOTTOM_RIGHT = 3
|
|
5
|
+
ALIGN_MIDDLE_LEFT = 4
|
|
6
|
+
ALIGN_MIDDLE_CENTER = 5
|
|
7
|
+
ALIGN_MIDDLE_RIGHT = 6
|
|
8
|
+
ALIGN_TOP_LEFT = 7
|
|
9
|
+
ALIGN_TOP_CENTER = 8
|
|
10
|
+
ALIGN_TOP_RIGHT = 9
|
|
11
|
+
|
|
12
|
+
# Legacy alignment mapping
|
|
13
|
+
LEGACY_ALIGNMENT_TO_REGULAR = {
|
|
14
|
+
"1": ALIGN_BOTTOM_LEFT,
|
|
15
|
+
"2": ALIGN_BOTTOM_CENTER,
|
|
16
|
+
"3": ALIGN_BOTTOM_RIGHT,
|
|
17
|
+
"5": ALIGN_TOP_LEFT,
|
|
18
|
+
"6": ALIGN_TOP_CENTER,
|
|
19
|
+
"7": ALIGN_TOP_RIGHT,
|
|
20
|
+
"9": ALIGN_MIDDLE_LEFT,
|
|
21
|
+
"10": ALIGN_MIDDLE_CENTER,
|
|
22
|
+
"11": ALIGN_MIDDLE_RIGHT,
|
|
23
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
class Event:
|
|
2
|
+
aliases = {}
|
|
3
|
+
formatters = None
|
|
4
|
+
order = [
|
|
5
|
+
"Layer",
|
|
6
|
+
"Start",
|
|
7
|
+
"End",
|
|
8
|
+
"Style",
|
|
9
|
+
"Name",
|
|
10
|
+
"MarginL",
|
|
11
|
+
"MarginR",
|
|
12
|
+
"MarginV",
|
|
13
|
+
"Effect",
|
|
14
|
+
"Text",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
# Constructor
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.type = None
|
|
20
|
+
|
|
21
|
+
self.Layer = 0
|
|
22
|
+
self.Start = 0.0
|
|
23
|
+
self.End = 0.0
|
|
24
|
+
self.Style = None
|
|
25
|
+
self.Name = ""
|
|
26
|
+
self.MarginL = 0
|
|
27
|
+
self.MarginR = 0
|
|
28
|
+
self.MarginV = 0
|
|
29
|
+
self.Effect = ""
|
|
30
|
+
self.Text = ""
|
|
31
|
+
|
|
32
|
+
def set(self, attribute_name, value, *args):
|
|
33
|
+
if hasattr(self, attribute_name) and attribute_name[0].isupper():
|
|
34
|
+
setattr(
|
|
35
|
+
self,
|
|
36
|
+
attribute_name,
|
|
37
|
+
self.formatters[attribute_name][0](value, *args),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def get(self, attribute_name, *args):
|
|
41
|
+
if hasattr(self, attribute_name) and attribute_name[0].isupper():
|
|
42
|
+
return self.formatters[attribute_name][1](getattr(self, attribute_name), *args)
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
def copy(self, other=None):
|
|
46
|
+
if other is None:
|
|
47
|
+
other = self.__class__()
|
|
48
|
+
target = other
|
|
49
|
+
source = self
|
|
50
|
+
else:
|
|
51
|
+
target = other
|
|
52
|
+
source = self
|
|
53
|
+
|
|
54
|
+
# Copy all attributes
|
|
55
|
+
target.type = source.type
|
|
56
|
+
target.Layer = source.Layer
|
|
57
|
+
target.Start = source.Start
|
|
58
|
+
target.End = source.End
|
|
59
|
+
target.Style = source.Style
|
|
60
|
+
target.Name = source.Name
|
|
61
|
+
target.MarginL = source.MarginL
|
|
62
|
+
target.MarginR = source.MarginR
|
|
63
|
+
target.MarginV = source.MarginV
|
|
64
|
+
target.Effect = source.Effect
|
|
65
|
+
target.Text = source.Text
|
|
66
|
+
|
|
67
|
+
return target
|
|
68
|
+
|
|
69
|
+
def equals(self, other):
|
|
70
|
+
return (
|
|
71
|
+
self.type == other.type
|
|
72
|
+
and self.Layer == other.Layer
|
|
73
|
+
and self.Start == other.Start
|
|
74
|
+
and self.End == other.End
|
|
75
|
+
and self.Style is other.Style
|
|
76
|
+
and self.Name == other.Name
|
|
77
|
+
and self.MarginL == other.MarginL
|
|
78
|
+
and self.MarginR == other.MarginR
|
|
79
|
+
and self.MarginV == other.MarginV
|
|
80
|
+
and self.Effect == other.Effect
|
|
81
|
+
and self.Text == other.Text
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def same_style(self, other):
|
|
85
|
+
return (
|
|
86
|
+
self.type == other.type
|
|
87
|
+
and self.Layer == other.Layer
|
|
88
|
+
and self.Style is other.Style
|
|
89
|
+
and self.Name == other.Name
|
|
90
|
+
and self.MarginL == other.MarginL
|
|
91
|
+
and self.MarginR == other.MarginR
|
|
92
|
+
and self.MarginV == other.MarginV
|
|
93
|
+
and self.Effect == other.Effect
|
|
94
|
+
)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Formatters:
|
|
5
|
+
__re_color_format = re.compile(r"&H([0-9a-fA-F]{8}|[0-9a-fA-F]{6})", re.U)
|
|
6
|
+
__re_tag_number = re.compile(r"^\s*([\+\-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+))", re.U)
|
|
7
|
+
|
|
8
|
+
@classmethod
|
|
9
|
+
def same(cls, val, *args):
|
|
10
|
+
return val
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def color_to_str(cls, val, *args):
|
|
14
|
+
return "&H{0:02X}{1:02X}{2:02X}{3:02X}".format(255 - val[3], val[2], val[1], val[0])
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def str_to_color(cls, val, *args):
|
|
18
|
+
match = cls.__re_color_format.search(val)
|
|
19
|
+
if match:
|
|
20
|
+
hex_val = "{0:>08s}".format(match.group(1))
|
|
21
|
+
return (
|
|
22
|
+
int(hex_val[6:8], 16), # Red
|
|
23
|
+
int(hex_val[4:6], 16), # Green
|
|
24
|
+
int(hex_val[2:4], 16), # Blue
|
|
25
|
+
255 - int(hex_val[0:2], 16), # Alpha
|
|
26
|
+
)
|
|
27
|
+
# Return white (255, 255, 255, 255) for invalid input
|
|
28
|
+
return (255, 255, 255, 255)
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def n1bool_to_str(cls, val, *args):
|
|
32
|
+
if val:
|
|
33
|
+
return "-1"
|
|
34
|
+
return "0"
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def str_to_n1bool(cls, val, *args):
|
|
38
|
+
try:
|
|
39
|
+
val = int(val, 10)
|
|
40
|
+
except ValueError:
|
|
41
|
+
return False
|
|
42
|
+
return val != 0
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def integer_to_str(cls, val, *args):
|
|
46
|
+
return str(int(val))
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def str_to_integer(cls, val, *args):
|
|
50
|
+
try:
|
|
51
|
+
return int(val, 10)
|
|
52
|
+
except ValueError:
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def number_to_str(cls, val, *args):
|
|
57
|
+
if int(val) == val:
|
|
58
|
+
return str(int(val))
|
|
59
|
+
# No decimal
|
|
60
|
+
return str(val)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def str_to_number(cls, val, *args):
|
|
64
|
+
try:
|
|
65
|
+
return float(val)
|
|
66
|
+
except ValueError:
|
|
67
|
+
return 0.0
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def timecode_to_str_generic(
|
|
71
|
+
cls,
|
|
72
|
+
timecode,
|
|
73
|
+
decimal_length=2,
|
|
74
|
+
seconds_length=2,
|
|
75
|
+
minutes_length=2,
|
|
76
|
+
hours_length=1,
|
|
77
|
+
):
|
|
78
|
+
if decimal_length > 0:
|
|
79
|
+
total_length = seconds_length + decimal_length + 1
|
|
80
|
+
else:
|
|
81
|
+
total_length = seconds_length
|
|
82
|
+
|
|
83
|
+
tc_parts = [
|
|
84
|
+
"{{0:0{0:d}d}}".format(hours_length).format(int(timecode // 3600)),
|
|
85
|
+
"{{0:0{0:d}d}}".format(minutes_length).format(int((timecode // 60) % 60)),
|
|
86
|
+
"{{0:0{0:d}.{1:d}f}}".format(total_length, decimal_length).format(timecode % 60),
|
|
87
|
+
]
|
|
88
|
+
return ":".join(tc_parts)
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def timecode_to_str(cls, val, *args):
|
|
92
|
+
return cls.timecode_to_str_generic(val, 2)
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def str_to_timecode(cls, val, *args):
|
|
96
|
+
time = 0.0
|
|
97
|
+
mult = 1
|
|
98
|
+
|
|
99
|
+
for t in reversed(val.split(":")):
|
|
100
|
+
time += float(t) * mult
|
|
101
|
+
mult *= 60
|
|
102
|
+
|
|
103
|
+
return time
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def style_to_str(cls, val, *args):
|
|
107
|
+
if val is None:
|
|
108
|
+
return ""
|
|
109
|
+
return val.Name
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def str_to_style(cls, val, style_map, style_constructor, *args):
|
|
113
|
+
if val in style_map:
|
|
114
|
+
return style_map[val]
|
|
115
|
+
|
|
116
|
+
# Create fake
|
|
117
|
+
style = style_constructor()
|
|
118
|
+
style.fake = True
|
|
119
|
+
style.Name = val
|
|
120
|
+
|
|
121
|
+
# Add to map (will not be included in global style list, but allows for duplicate "fake" styles to reference the same object)
|
|
122
|
+
style_map[style.Name] = style
|
|
123
|
+
|
|
124
|
+
# Return the new style
|
|
125
|
+
return style
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def tag_argument_to_number(cls, arg, default_value=None):
|
|
129
|
+
match = cls.__re_tag_number.match(arg)
|
|
130
|
+
if match is None:
|
|
131
|
+
return default_value
|
|
132
|
+
return float(match.group(1))
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional, Tuple, List
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from lyrics_transcriber.types import LyricsSegment
|
|
9
|
+
from lyrics_transcriber.output.ass.event import Event
|
|
10
|
+
from lyrics_transcriber.output.ass.style import Style
|
|
11
|
+
from lyrics_transcriber.output.ass.config import LineState, ScreenConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class LyricsLine:
|
|
16
|
+
"""Represents a single line of lyrics with timing and karaoke information."""
|
|
17
|
+
|
|
18
|
+
segment: LyricsSegment
|
|
19
|
+
screen_config: ScreenConfig
|
|
20
|
+
logger: Optional[logging.Logger] = None
|
|
21
|
+
previous_end_time: Optional[float] = None
|
|
22
|
+
|
|
23
|
+
def __post_init__(self):
|
|
24
|
+
"""Ensure logger is initialized"""
|
|
25
|
+
if self.logger is None:
|
|
26
|
+
self.logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
def _get_font(self, style: Style) -> ImageFont.FreeTypeFont:
|
|
29
|
+
"""Get the font for text measurements."""
|
|
30
|
+
# ASS renders fonts about 70% of their actual size
|
|
31
|
+
ASS_FONT_SCALE = 0.70
|
|
32
|
+
|
|
33
|
+
# Scale down the font size to match ASS rendering
|
|
34
|
+
adjusted_size = int(style.Fontsize * ASS_FONT_SCALE)
|
|
35
|
+
self.logger.debug(f"Adjusting font size from {style.Fontsize} to {adjusted_size} to match ASS rendering")
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
# Use the Fontpath property from Style class
|
|
39
|
+
if style.Fontpath and os.path.exists(style.Fontpath):
|
|
40
|
+
return ImageFont.truetype(style.Fontpath, size=adjusted_size)
|
|
41
|
+
self.logger.warning(f"Could not load font {style.Fontpath}, using default")
|
|
42
|
+
return ImageFont.load_default()
|
|
43
|
+
except (OSError, AttributeError) as e:
|
|
44
|
+
self.logger.warning(f"Font error ({e}), using default")
|
|
45
|
+
return ImageFont.load_default()
|
|
46
|
+
|
|
47
|
+
def _get_text_dimensions(self, text: str, font: ImageFont.FreeTypeFont) -> Tuple[int, int]:
|
|
48
|
+
"""Get the pixel dimensions of rendered text."""
|
|
49
|
+
# Create an image the same size as the video frame
|
|
50
|
+
img = Image.new("RGB", (self.screen_config.video_width, self.screen_config.video_height), color="black")
|
|
51
|
+
draw = ImageDraw.Draw(img)
|
|
52
|
+
|
|
53
|
+
# Get the bounding box
|
|
54
|
+
bbox = draw.textbbox((0, 0), text, font=font)
|
|
55
|
+
width = bbox[2] - bbox[0]
|
|
56
|
+
height = bbox[3] - bbox[1]
|
|
57
|
+
|
|
58
|
+
self.logger.debug(f"Text dimensions for '{text}': width={width}px, height={height}px")
|
|
59
|
+
self.logger.debug(f"Video dimensions: {self.screen_config.video_width}x{self.screen_config.video_height}")
|
|
60
|
+
return width, height
|
|
61
|
+
|
|
62
|
+
# fmt: off
|
|
63
|
+
def _create_lead_in_text(self, state: LineState) -> Tuple[str, bool]:
|
|
64
|
+
"""Create lead-in indicator text if needed.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Tuple of (text, has_lead_in)
|
|
68
|
+
"""
|
|
69
|
+
has_lead_in = (self.previous_end_time is None or
|
|
70
|
+
self.segment.start_time - self.previous_end_time >= self.screen_config.lead_in_gap_threshold)
|
|
71
|
+
|
|
72
|
+
if not has_lead_in:
|
|
73
|
+
return "", False
|
|
74
|
+
|
|
75
|
+
# Add a hyphen with karaoke timing for the last 2 seconds before the line
|
|
76
|
+
lead_in_start = max(state.timing.fade_in_time, self.segment.start_time - 2.0)
|
|
77
|
+
gap_before_highlight = int((lead_in_start - state.timing.fade_in_time) * 100)
|
|
78
|
+
highlight_duration = int((self.segment.start_time - lead_in_start) * 100)
|
|
79
|
+
|
|
80
|
+
text = ""
|
|
81
|
+
# Add initial gap if needed
|
|
82
|
+
if gap_before_highlight > 0:
|
|
83
|
+
text += f"{{\\k{gap_before_highlight}}}"
|
|
84
|
+
# Add the hyphen with highlight
|
|
85
|
+
text += f"{{\\kf{highlight_duration}}}→ "
|
|
86
|
+
|
|
87
|
+
return text, True
|
|
88
|
+
|
|
89
|
+
def _create_lead_in_event(self, state: LineState, style: Style, video_width: int, config: ScreenConfig) -> Optional[Event]:
|
|
90
|
+
"""Create a separate event for the lead-in indicator if needed."""
|
|
91
|
+
# Check if lead-in is enabled
|
|
92
|
+
if not config.lead_in_enabled:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
# Check if there's a sufficient gap to show lead-in
|
|
96
|
+
if not (self.previous_end_time is None or
|
|
97
|
+
self.segment.start_time - self.previous_end_time >= config.lead_in_gap_threshold):
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
self.logger.debug(f"Creating lead-in indicator for line: '{self.segment.text}'")
|
|
101
|
+
|
|
102
|
+
# Calculate all timing points
|
|
103
|
+
line_start = self.segment.start_time
|
|
104
|
+
appear_time = line_start - 3.0 # Start 3 seconds before line
|
|
105
|
+
fade_in_end = appear_time + 0.8 # 800ms fade in
|
|
106
|
+
fade_out_start = line_start - 0.3 # Start fade 300ms before reaching final position
|
|
107
|
+
fade_out_end = line_start + 0.2 # Complete fade 200ms after line starts (500ms total fade)
|
|
108
|
+
|
|
109
|
+
self.logger.debug(f"Timing calculations:")
|
|
110
|
+
self.logger.debug(f" Line starts at: {line_start:.2f}s")
|
|
111
|
+
self.logger.debug(f" Rectangle appears at: {appear_time:.2f}s")
|
|
112
|
+
self.logger.debug(f" Fade in completes at: {fade_in_end:.2f}s")
|
|
113
|
+
self.logger.debug(f" Fade out starts at: {fade_out_start:.2f}s")
|
|
114
|
+
self.logger.debug(f" Rectangle reaches final position at: {line_start:.2f}s")
|
|
115
|
+
self.logger.debug(f" Rectangle fully faded out at: {fade_out_end:.2f}s")
|
|
116
|
+
|
|
117
|
+
# Calculate dimensions and positions using configurable percentages
|
|
118
|
+
font = self._get_font(style)
|
|
119
|
+
# Apply case transformation to match the actual rendered text
|
|
120
|
+
main_text = self._apply_case_transform(self.segment.text)
|
|
121
|
+
main_width, main_height = self._get_text_dimensions(main_text, font)
|
|
122
|
+
rect_width = int(self.screen_config.video_width * (config.lead_in_width_percent / 100))
|
|
123
|
+
rect_height = int(self.screen_config.video_height * (config.lead_in_height_percent / 100))
|
|
124
|
+
# Calculate where the left edge of the centered text will be
|
|
125
|
+
text_left = self.screen_config.video_width//2 - main_width//2
|
|
126
|
+
# Apply horizontal offset if configured
|
|
127
|
+
horizontal_offset = int(self.screen_config.video_width * (config.lead_in_horiz_offset_percent / 100))
|
|
128
|
+
final_x_position = text_left + horizontal_offset
|
|
129
|
+
# Apply vertical offset if configured
|
|
130
|
+
vertical_offset = int(self.screen_config.video_height * (config.lead_in_vert_offset_percent / 100))
|
|
131
|
+
final_y_position = state.y_position + main_height + vertical_offset
|
|
132
|
+
|
|
133
|
+
self.logger.debug(f"Position calculations:")
|
|
134
|
+
self.logger.debug(f" Video dimensions: {self.screen_config.video_width}x{self.screen_config.video_height}")
|
|
135
|
+
self.logger.debug(f" Original text: '{self.segment.text}'")
|
|
136
|
+
self.logger.debug(f" Transformed text: '{main_text}'")
|
|
137
|
+
self.logger.debug(f" Main text width: {main_width}px")
|
|
138
|
+
self.logger.debug(f" Main text height: {main_height}px")
|
|
139
|
+
self.logger.debug(f" Rectangle dimensions: {rect_width}x{rect_height}px (from {config.lead_in_width_percent}% x {config.lead_in_height_percent}%)")
|
|
140
|
+
self.logger.debug(f" Text left edge: {text_left}px")
|
|
141
|
+
self.logger.debug(f" Horizontal offset: {horizontal_offset}px ({config.lead_in_horiz_offset_percent}% of screen width)")
|
|
142
|
+
self.logger.debug(f" Final X position: {final_x_position}px")
|
|
143
|
+
self.logger.debug(f" Vertical offset: {vertical_offset}px ({config.lead_in_vert_offset_percent}% of screen height)")
|
|
144
|
+
self.logger.debug(f" Final Y position: {final_y_position}px")
|
|
145
|
+
self.logger.debug(f" Vertical position: {state.y_position}px")
|
|
146
|
+
|
|
147
|
+
# Create main indicator event
|
|
148
|
+
main_event = Event()
|
|
149
|
+
main_event.type = "Dialogue"
|
|
150
|
+
main_event.Layer = 0
|
|
151
|
+
main_event.Style = style
|
|
152
|
+
main_event.Start = appear_time
|
|
153
|
+
main_event.End = fade_out_end
|
|
154
|
+
|
|
155
|
+
# Calculate movement duration in milliseconds
|
|
156
|
+
move_duration = int((line_start - appear_time) * 1000)
|
|
157
|
+
|
|
158
|
+
# Build the indicator rectangle text with configurable styling
|
|
159
|
+
main_text = (
|
|
160
|
+
f"{{\\an8}}" # center-bottom alignment
|
|
161
|
+
f"{{\\move(0,{final_y_position},{final_x_position},{final_y_position},0,{move_duration})}}" # Move until line start
|
|
162
|
+
f"{{\\c{config.get_lead_in_color_ass_format()}}}" # Configurable lead-in color in ASS format
|
|
163
|
+
f"{{\\alpha{config.get_lead_in_opacity_ass_format()}}}" # Configurable opacity
|
|
164
|
+
f"{{\\fad(800,500)}}" # 800ms fade in, 500ms fade out
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Add outline if thickness > 0
|
|
168
|
+
if config.lead_in_outline_thickness > 0:
|
|
169
|
+
main_text += (
|
|
170
|
+
f"{{\\3c{config.get_lead_in_outline_color_ass_format()}}}" # Outline color
|
|
171
|
+
f"{{\\bord{config.lead_in_outline_thickness}}}" # Outline thickness
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
main_text += f"{{\\bord0}}" # No outline
|
|
175
|
+
|
|
176
|
+
# Add the rectangle shape
|
|
177
|
+
main_text += f"{{\\p1}}m {-rect_width} {-rect_height} l 0 {-rect_height} 0 0 {-rect_width} 0{{\\p0}}" # Draw up from bottom
|
|
178
|
+
|
|
179
|
+
main_event.Text = main_text
|
|
180
|
+
|
|
181
|
+
return [main_event]
|
|
182
|
+
|
|
183
|
+
def create_ass_events(
|
|
184
|
+
self,
|
|
185
|
+
state: LineState,
|
|
186
|
+
style: Style,
|
|
187
|
+
config: ScreenConfig,
|
|
188
|
+
previous_end_time: Optional[float] = None
|
|
189
|
+
) -> List[Event]:
|
|
190
|
+
"""Create ASS events for this line. Returns [main_event] or [lead_in_event, main_event]."""
|
|
191
|
+
self.previous_end_time = previous_end_time
|
|
192
|
+
events = []
|
|
193
|
+
|
|
194
|
+
# Create lead-in event if needed
|
|
195
|
+
lead_in_event = self._create_lead_in_event(state, style, config.video_width, config)
|
|
196
|
+
if lead_in_event:
|
|
197
|
+
events.extend(lead_in_event)
|
|
198
|
+
|
|
199
|
+
# Create main lyrics event
|
|
200
|
+
main_event = Event()
|
|
201
|
+
main_event.type = "Dialogue"
|
|
202
|
+
main_event.Layer = 0
|
|
203
|
+
main_event.Style = style
|
|
204
|
+
main_event.Start = state.timing.fade_in_time
|
|
205
|
+
main_event.End = state.timing.end_time
|
|
206
|
+
|
|
207
|
+
# Use absolute positioning
|
|
208
|
+
x_pos = config.video_width // 2 # Center horizontally
|
|
209
|
+
|
|
210
|
+
# Main lyrics text with positioning and fade
|
|
211
|
+
text = (
|
|
212
|
+
f"{{\\an8}}{{\\pos({x_pos},{state.y_position})}}"
|
|
213
|
+
f"{{\\fad({config.fade_in_ms},{config.fade_out_ms})}}"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Add the main lyrics text with karaoke timing
|
|
217
|
+
text += self._create_ass_text(timedelta(seconds=state.timing.fade_in_time))
|
|
218
|
+
|
|
219
|
+
main_event.Text = text
|
|
220
|
+
events.append(main_event)
|
|
221
|
+
|
|
222
|
+
return events
|
|
223
|
+
|
|
224
|
+
def _apply_case_transform(self, text: str) -> str:
|
|
225
|
+
"""Apply case transformation to text based on screen config setting."""
|
|
226
|
+
transform = getattr(self.screen_config, 'text_case_transform', 'none')
|
|
227
|
+
|
|
228
|
+
if transform == "uppercase":
|
|
229
|
+
return text.upper()
|
|
230
|
+
elif transform == "lowercase":
|
|
231
|
+
return text.lower()
|
|
232
|
+
elif transform == "propercase":
|
|
233
|
+
return text.title()
|
|
234
|
+
else: # "none" or any other value
|
|
235
|
+
return text
|
|
236
|
+
|
|
237
|
+
def _create_ass_text(self, start_ts: timedelta) -> str:
|
|
238
|
+
"""Create the ASS text with karaoke timing tags."""
|
|
239
|
+
# Initial delay before first word
|
|
240
|
+
first_word_time = self.segment.start_time
|
|
241
|
+
|
|
242
|
+
# Add initial delay for regular lines
|
|
243
|
+
start_time = max(0, (first_word_time - start_ts.total_seconds()) * 100)
|
|
244
|
+
text = r"{\k" + str(int(round(start_time))) + r"}"
|
|
245
|
+
|
|
246
|
+
prev_end_time = first_word_time
|
|
247
|
+
|
|
248
|
+
for word in self.segment.words:
|
|
249
|
+
# Add gap between words if needed
|
|
250
|
+
gap = word.start_time - prev_end_time
|
|
251
|
+
if gap > 0.1: # Only add gap if significant
|
|
252
|
+
text += r"{\k" + str(int(round(gap * 100))) + r"}"
|
|
253
|
+
|
|
254
|
+
# Add the word with its duration
|
|
255
|
+
duration = int(round((word.end_time - word.start_time) * 100))
|
|
256
|
+
# Apply case transformation to the word text
|
|
257
|
+
transformed_text = self._apply_case_transform(word.text)
|
|
258
|
+
text += r"{\kf" + str(duration) + r"}" + transformed_text + " "
|
|
259
|
+
|
|
260
|
+
prev_end_time = word.end_time # Track the actual end time of the word
|
|
261
|
+
|
|
262
|
+
return text.rstrip()
|
|
263
|
+
|
|
264
|
+
def __str__(self):
|
|
265
|
+
return f"{{{self.segment.text}}}"
|