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,2088 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
import os, re, sys, functools, collections
|
|
3
|
+
from lyrics_transcriber.output.ass.event import Event
|
|
4
|
+
from lyrics_transcriber.output.ass.style import Style
|
|
5
|
+
from lyrics_transcriber.output.ass.formatters import Formatters
|
|
6
|
+
from lyrics_transcriber.output.ass.constants import (
|
|
7
|
+
ALIGN_BOTTOM_LEFT,
|
|
8
|
+
ALIGN_BOTTOM_CENTER,
|
|
9
|
+
ALIGN_BOTTOM_RIGHT,
|
|
10
|
+
ALIGN_MIDDLE_LEFT,
|
|
11
|
+
ALIGN_MIDDLE_CENTER,
|
|
12
|
+
ALIGN_MIDDLE_RIGHT,
|
|
13
|
+
ALIGN_TOP_LEFT,
|
|
14
|
+
ALIGN_TOP_CENTER,
|
|
15
|
+
ALIGN_TOP_RIGHT,
|
|
16
|
+
LEGACY_ALIGNMENT_TO_REGULAR,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
version_info = (1, 0, 4)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Advanced SubStation Alpha read/write/modification class
|
|
23
|
+
class ASS:
|
|
24
|
+
|
|
25
|
+
Event.formatters = {
|
|
26
|
+
"Layer": (Formatters.str_to_integer, Formatters.integer_to_str),
|
|
27
|
+
"Start": (Formatters.str_to_timecode, Formatters.timecode_to_str),
|
|
28
|
+
"End": (Formatters.str_to_timecode, Formatters.timecode_to_str),
|
|
29
|
+
"Style": (Formatters.str_to_style, Formatters.style_to_str),
|
|
30
|
+
"Name": (Formatters.same, Formatters.same),
|
|
31
|
+
"MarginL": (Formatters.str_to_integer, Formatters.integer_to_str),
|
|
32
|
+
"MarginR": (Formatters.str_to_integer, Formatters.integer_to_str),
|
|
33
|
+
"MarginV": (Formatters.str_to_integer, Formatters.integer_to_str),
|
|
34
|
+
"Effect": (Formatters.same, Formatters.same),
|
|
35
|
+
"Text": (Formatters.same, Formatters.same),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class Info:
|
|
39
|
+
# Constructor
|
|
40
|
+
def __init__(self, key, value):
|
|
41
|
+
self.key = key
|
|
42
|
+
self.value = value
|
|
43
|
+
|
|
44
|
+
__re_ass_read_section_label = re.compile(r"^(?:\[(.+)\])$", re.U)
|
|
45
|
+
__re_ass_read_key_value = re.compile(r"^([^:]+):\s?(.+)$", re.U)
|
|
46
|
+
__re_tag_block = re.compile(r"(\{)(.*?)(\})", re.U)
|
|
47
|
+
__re_tag_block_or_special = re.compile(r"(\{)(.+?)(\})|(\\[hnN])", re.U)
|
|
48
|
+
__tags_with_parentheses = {
|
|
49
|
+
"t": True,
|
|
50
|
+
"fad": True,
|
|
51
|
+
"org": True,
|
|
52
|
+
"pos": True,
|
|
53
|
+
"clip": True,
|
|
54
|
+
"fade": True,
|
|
55
|
+
"move": True,
|
|
56
|
+
"iclip": True,
|
|
57
|
+
}
|
|
58
|
+
__tags_transformable = {
|
|
59
|
+
"c": 1,
|
|
60
|
+
"1c": 1,
|
|
61
|
+
"2c": 1,
|
|
62
|
+
"3c": 1,
|
|
63
|
+
"4c": 1,
|
|
64
|
+
"alpha": 1,
|
|
65
|
+
"1a": 1,
|
|
66
|
+
"2a": 1,
|
|
67
|
+
"3a": 1,
|
|
68
|
+
"4a": 1,
|
|
69
|
+
"fs": 1,
|
|
70
|
+
"fr": 1,
|
|
71
|
+
"frx": 1,
|
|
72
|
+
"fry": 1,
|
|
73
|
+
"frz": 1,
|
|
74
|
+
"fscx": 1,
|
|
75
|
+
"fscy": 1,
|
|
76
|
+
"fsp": 1,
|
|
77
|
+
"bord": 1,
|
|
78
|
+
"xbord": 1,
|
|
79
|
+
"ybord": 1,
|
|
80
|
+
"shad": 1,
|
|
81
|
+
"xshad": 1,
|
|
82
|
+
"yshad": 1,
|
|
83
|
+
"clip": 4,
|
|
84
|
+
"iclip": 4,
|
|
85
|
+
"blur": 1,
|
|
86
|
+
"be": 1,
|
|
87
|
+
"fax": 1,
|
|
88
|
+
"fay": 1,
|
|
89
|
+
}
|
|
90
|
+
__tags_animated = {
|
|
91
|
+
"t": True,
|
|
92
|
+
"k": True,
|
|
93
|
+
"K": True,
|
|
94
|
+
"kf": True,
|
|
95
|
+
"ko": True,
|
|
96
|
+
"move": True,
|
|
97
|
+
"fad": True,
|
|
98
|
+
"fade": True,
|
|
99
|
+
}
|
|
100
|
+
__re_tag = re.compile(
|
|
101
|
+
r"""\\(?:
|
|
102
|
+
(?:(fad|pos|org) \( ([^\\]+?) , ([^\\]+?) \) ) |
|
|
103
|
+
(?:(move) \( ([^\\]+?) , ([^\\]+?) , ([^\\]+?) , ([^\\]+?) (?:, ([^\\]+?) , ([^\\]+?))? \) ) |
|
|
104
|
+
(?:(fade) \( ([^\\]+?) , ([^\\]+?) , ([^\\]+?) , ([^\\]+?) , ([^\\]+?) , ([^\\]+?) , ([^\\]+?) \) ) |
|
|
105
|
+
(?:(clip|iclip) \( ([^\\]+?) (?:, ([^\\]+?) (?:, ([^\\]+?) , ([^\\]+?))?)? \) ) |
|
|
106
|
+
(?:(t) \( ([^,]+?) (?:, ([^,]+?) (?:, ([^,]+?) (?:, ([^,]+?))?)?)? \) ) |
|
|
107
|
+
(?:(c|1c|2c|3c|4c) (&?H? [0-9a-fA-F]{1,6} &?) ) |
|
|
108
|
+
(?:(alpha|1a|2a|3a|4a) (&?H? [0-9a-fA-F]{1,2} &?) ) |
|
|
109
|
+
(i0|i1|u0|u1|s0|s1) |
|
|
110
|
+
(?:(r) ([^\\]+)?) |
|
|
111
|
+
(?:(xbord|xshad|ybord|yshad | bord|blur|fscx|fscy|shad | fax|fay|frx|fry|frz|fsp|pbo | an|be|fe|fn|fs|fr|kf|ko | a|b|k|K|p|q) ([^\\]+))
|
|
112
|
+
)()??""",
|
|
113
|
+
re.VERBOSE | re.U,
|
|
114
|
+
)
|
|
115
|
+
__re_draw_command = re.compile(r"([a-zA-Z]+)((?:\s+(?:[\+\-]?[0-9]+))*)", re.U)
|
|
116
|
+
__re_remove_special = re.compile(r"(\s*)(?:\\([hnN]))(\s*)")
|
|
117
|
+
__re_filename_format = (re.compile(r".py[co]$"), ".py")
|
|
118
|
+
__re_draw_command_split = re.compile(r"\s+")
|
|
119
|
+
__re_draw_commands_ord_min = ord("a")
|
|
120
|
+
__re_draw_commands_ord_max = ord("z")
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def __split_line(cls, line, split_time, naive):
|
|
124
|
+
if split_time <= line.Start or split_time >= line.End:
|
|
125
|
+
return None
|
|
126
|
+
# Nothing to split
|
|
127
|
+
|
|
128
|
+
modify_tag = None
|
|
129
|
+
if not naive:
|
|
130
|
+
modify_tag = lambda t: cls.__split_line_modify_tag(t, split_time)
|
|
131
|
+
|
|
132
|
+
# Before
|
|
133
|
+
before = line.copy()
|
|
134
|
+
before.End = split_time
|
|
135
|
+
before.Text = cls.parse_text(before.Text, modify_tag=modify_tag)
|
|
136
|
+
|
|
137
|
+
# After
|
|
138
|
+
after = line.copy()
|
|
139
|
+
after.Start = split_time
|
|
140
|
+
after.Text = cls.parse_text(after.Text, modify_tag=modify_tag)
|
|
141
|
+
|
|
142
|
+
# Done
|
|
143
|
+
return (before, after)
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def __split_line3(cls, line, split_time, naive=False):
|
|
147
|
+
if split_time < line.Start or split_time > line.End:
|
|
148
|
+
return None
|
|
149
|
+
# Nothing to split
|
|
150
|
+
|
|
151
|
+
modify_tag = None
|
|
152
|
+
if not naive:
|
|
153
|
+
modify_tag = lambda t: cls.__split_line_modify_tag(t, split_time)
|
|
154
|
+
|
|
155
|
+
# Before
|
|
156
|
+
if line.Start < split_time:
|
|
157
|
+
before = line.copy()
|
|
158
|
+
before.End = split_time
|
|
159
|
+
before.Text = cls.parse_text(before.Text, modify_tag=modify_tag)
|
|
160
|
+
else:
|
|
161
|
+
before = None
|
|
162
|
+
|
|
163
|
+
# After
|
|
164
|
+
if line.End > split_time:
|
|
165
|
+
after = line.copy()
|
|
166
|
+
after.Start = split_time
|
|
167
|
+
after.Text = cls.parse_text(after.Text, modify_tag=modify_tag)
|
|
168
|
+
else:
|
|
169
|
+
after = None
|
|
170
|
+
|
|
171
|
+
# Middle part
|
|
172
|
+
middle = line.copy()
|
|
173
|
+
middle.Start = split_time
|
|
174
|
+
middle.End = split_time
|
|
175
|
+
middle.Text = cls.parse_text(middle.Text, modify_tag=modify_tag)
|
|
176
|
+
|
|
177
|
+
return (before, middle, after)
|
|
178
|
+
|
|
179
|
+
@classmethod
|
|
180
|
+
def __split_line_modify_tag(cls, tag, split_time):
|
|
181
|
+
# This may better modify tags later, for now it's also a naive copy
|
|
182
|
+
return [tag]
|
|
183
|
+
|
|
184
|
+
__same_time_max_delta = 1.0e-5
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def __join_lines(cls, line1, line2, naive):
|
|
188
|
+
if abs(line2.End - line1.Start) <= cls.__same_time_max_delta:
|
|
189
|
+
# Flip
|
|
190
|
+
linetemp = line1
|
|
191
|
+
line1 = line2
|
|
192
|
+
line2 = linetemp
|
|
193
|
+
|
|
194
|
+
# Join check
|
|
195
|
+
line_join = None
|
|
196
|
+
if abs(line1.End - line2.Start) <= cls.__same_time_max_delta:
|
|
197
|
+
# Might be joinable
|
|
198
|
+
if line1.Text == line2.Text:
|
|
199
|
+
# Check if there are no animations
|
|
200
|
+
if naive or not cls.__line_has_animations(line1.Text):
|
|
201
|
+
# Copy and return
|
|
202
|
+
line_join = line1.copy()
|
|
203
|
+
line_join.End = line2.End
|
|
204
|
+
|
|
205
|
+
# Not joinable
|
|
206
|
+
return line_join
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def __line_has_animations(cls, text):
|
|
210
|
+
state = {
|
|
211
|
+
"animations": 0,
|
|
212
|
+
}
|
|
213
|
+
cls.parse_text(text, modify_tag=lambda t: cls.__line_has_animations_modify_tag(state, t))
|
|
214
|
+
return state["animations"] > 0
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def __line_has_animations_modify_tag(cls, state, tag):
|
|
218
|
+
if tag[0] in cls.__tags_animated:
|
|
219
|
+
state["animations"] += 1
|
|
220
|
+
|
|
221
|
+
return [tag]
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def __kwarg_default(cls, kwargs, key, default_value):
|
|
225
|
+
if key in kwargs:
|
|
226
|
+
return kwargs[key]
|
|
227
|
+
return default_value
|
|
228
|
+
|
|
229
|
+
def __change_event_styles(self, style_src, style_dest):
|
|
230
|
+
for line in self.events:
|
|
231
|
+
if line.Style is style_src:
|
|
232
|
+
line.Style = style_dest
|
|
233
|
+
|
|
234
|
+
def __get_minimum_timecode(self):
|
|
235
|
+
if len(self.events) == 0:
|
|
236
|
+
return 0.0
|
|
237
|
+
|
|
238
|
+
t = self.events[0].Start
|
|
239
|
+
for i in range(1, len(self.events)):
|
|
240
|
+
t2 = self.events[i].Start
|
|
241
|
+
if t2 < t:
|
|
242
|
+
t = t2
|
|
243
|
+
|
|
244
|
+
return t
|
|
245
|
+
|
|
246
|
+
def __get_maximum_timecode(self):
|
|
247
|
+
if len(self.events) == 0:
|
|
248
|
+
return 0.0
|
|
249
|
+
|
|
250
|
+
t = self.events[0].End
|
|
251
|
+
for i in range(1, len(self.events)):
|
|
252
|
+
t2 = self.events[i].End
|
|
253
|
+
if t2 > t:
|
|
254
|
+
t = t2
|
|
255
|
+
|
|
256
|
+
return t
|
|
257
|
+
|
|
258
|
+
def __range_cut(self, filter_types, start, end, naive):
|
|
259
|
+
# Split
|
|
260
|
+
if start is not None or end is not None:
|
|
261
|
+
i = 0
|
|
262
|
+
i_max = len(self.events)
|
|
263
|
+
while i < i_max:
|
|
264
|
+
line = self.events[i]
|
|
265
|
+
if filter_types is None or line.type in filter_types:
|
|
266
|
+
# Must be dialogue
|
|
267
|
+
if start is not None:
|
|
268
|
+
# Split
|
|
269
|
+
line_split = self.__split_line(line, start, naive=naive)
|
|
270
|
+
if line_split is not None:
|
|
271
|
+
line = line_split[1]
|
|
272
|
+
self.events[i] = line
|
|
273
|
+
self.events.append(line_split[0])
|
|
274
|
+
|
|
275
|
+
if end is not None:
|
|
276
|
+
# Split
|
|
277
|
+
line_split = self.__split_line(line, end, naive=naive)
|
|
278
|
+
if line_split is not None:
|
|
279
|
+
self.events[i] = line_split[0]
|
|
280
|
+
self.events.append(line_split[1])
|
|
281
|
+
|
|
282
|
+
# Next
|
|
283
|
+
i += 1
|
|
284
|
+
|
|
285
|
+
def __range_action(self, filter_types, start, end, full_inclusion, inverse, action):
|
|
286
|
+
# Modify lines
|
|
287
|
+
i = 0
|
|
288
|
+
i_max = len(self.events)
|
|
289
|
+
while i < i_max:
|
|
290
|
+
line = self.events[i]
|
|
291
|
+
if filter_types is None or line.type in filter_types:
|
|
292
|
+
if full_inclusion:
|
|
293
|
+
perform = (start is None or line.Start >= start) and (end is None or line.End <= end)
|
|
294
|
+
else:
|
|
295
|
+
perform = (start is None or line.End > start) and (end is None or line.Start < end)
|
|
296
|
+
|
|
297
|
+
if perform ^ inverse:
|
|
298
|
+
# action should return None if the line should be removed, else it should return an Event object (likely the same one that was input)
|
|
299
|
+
# action should NOT remove/add any events
|
|
300
|
+
line_res = action(line)
|
|
301
|
+
if line_res is None:
|
|
302
|
+
self.events.pop(i)
|
|
303
|
+
i_max -= 1
|
|
304
|
+
continue
|
|
305
|
+
elif line_res is not line:
|
|
306
|
+
self.events[i] = line_res
|
|
307
|
+
|
|
308
|
+
# Next
|
|
309
|
+
i += 1
|
|
310
|
+
|
|
311
|
+
def __set_script_info(self, key, value):
|
|
312
|
+
if key not in self.script_info:
|
|
313
|
+
instance = self.Info(key, value)
|
|
314
|
+
self.script_info_ordered.append(instance)
|
|
315
|
+
self.script_info[key] = instance
|
|
316
|
+
else:
|
|
317
|
+
self.script_info[key].value = value
|
|
318
|
+
|
|
319
|
+
@classmethod
|
|
320
|
+
def __legacy_align_to_regular(cls, value, default_value=None):
|
|
321
|
+
value = str(value)
|
|
322
|
+
if value in cls.__legacy_alignment_to_regular:
|
|
323
|
+
return cls.__legacy_alignment_to_regular[value]
|
|
324
|
+
return default_value
|
|
325
|
+
|
|
326
|
+
# Python 2/3 support
|
|
327
|
+
if sys.version_info[0] == 3:
|
|
328
|
+
# Version 3
|
|
329
|
+
@classmethod
|
|
330
|
+
def __py_2or3_var_is_string(cls, obj):
|
|
331
|
+
return isinstance(obj, str)
|
|
332
|
+
|
|
333
|
+
else:
|
|
334
|
+
# Version 2
|
|
335
|
+
@classmethod
|
|
336
|
+
def __py_2or3_var_is_string(cls, obj):
|
|
337
|
+
return isinstance(obj, basestring)
|
|
338
|
+
|
|
339
|
+
# Constructor
|
|
340
|
+
def __init__(self):
|
|
341
|
+
self.script_info_ordered = []
|
|
342
|
+
self.script_info = {}
|
|
343
|
+
|
|
344
|
+
self.styles_format = []
|
|
345
|
+
self.styles = []
|
|
346
|
+
|
|
347
|
+
self.events_format = []
|
|
348
|
+
self.events = []
|
|
349
|
+
|
|
350
|
+
# Reading/writing
|
|
351
|
+
def read(self, filename):
|
|
352
|
+
# Clear
|
|
353
|
+
self.script_info_ordered = []
|
|
354
|
+
self.script_info = {}
|
|
355
|
+
|
|
356
|
+
self.styles_format = []
|
|
357
|
+
self.styles = []
|
|
358
|
+
styles_map = {}
|
|
359
|
+
|
|
360
|
+
self.events_format = []
|
|
361
|
+
self.events = []
|
|
362
|
+
|
|
363
|
+
# Read and decode
|
|
364
|
+
f = open(filename, "rb")
|
|
365
|
+
s = f.read()
|
|
366
|
+
f.close()
|
|
367
|
+
|
|
368
|
+
s = s.decode("utf-8")
|
|
369
|
+
# Decode using UTF-8
|
|
370
|
+
s = s.replace("\ufeff", "")
|
|
371
|
+
# Replace any BOM
|
|
372
|
+
|
|
373
|
+
# Target region
|
|
374
|
+
target_format = None
|
|
375
|
+
target_map = None
|
|
376
|
+
target_map_key_getter = None
|
|
377
|
+
target_list = None
|
|
378
|
+
target_class = None
|
|
379
|
+
target_class_set_args = None
|
|
380
|
+
|
|
381
|
+
# Iterate over each line
|
|
382
|
+
lines = s.splitlines()
|
|
383
|
+
for i in range(len(lines)):
|
|
384
|
+
line = lines[i]
|
|
385
|
+
|
|
386
|
+
# [Labeled Section]
|
|
387
|
+
match = self.__re_ass_read_section_label.match(line)
|
|
388
|
+
if match is not None:
|
|
389
|
+
line = match.group(1)
|
|
390
|
+
if line == "Script Info":
|
|
391
|
+
target_format = None
|
|
392
|
+
target_map = self.script_info
|
|
393
|
+
target_map_key_getter = lambda i: i.key
|
|
394
|
+
target_list = self.script_info_ordered
|
|
395
|
+
target_class = None
|
|
396
|
+
target_class_set_args = None
|
|
397
|
+
elif line == "V4 Styles" or line == "V4+ Styles":
|
|
398
|
+
target_format = self.styles_format
|
|
399
|
+
target_map = styles_map
|
|
400
|
+
target_map_key_getter = lambda i: i.Name
|
|
401
|
+
target_list = self.styles
|
|
402
|
+
target_class = self.Style
|
|
403
|
+
target_class_set_args = []
|
|
404
|
+
elif line == "Events":
|
|
405
|
+
target_format = self.events_format
|
|
406
|
+
target_map = None
|
|
407
|
+
target_map_key_getter = None
|
|
408
|
+
target_list = self.events
|
|
409
|
+
target_class = self.Event
|
|
410
|
+
target_class_set_args = [styles_map, self.Style]
|
|
411
|
+
else:
|
|
412
|
+
# Invalid or not supported
|
|
413
|
+
target = None
|
|
414
|
+
elif target_list is None:
|
|
415
|
+
# No target
|
|
416
|
+
pass
|
|
417
|
+
elif len(line) == 0 or line[0] == ";":
|
|
418
|
+
# Comment or empty line
|
|
419
|
+
pass
|
|
420
|
+
else:
|
|
421
|
+
match = self.__re_ass_read_key_value.match(line)
|
|
422
|
+
if match is not None:
|
|
423
|
+
# Valid
|
|
424
|
+
if target_format is None:
|
|
425
|
+
# Direct map [Script Info]
|
|
426
|
+
instance = self.Info(match.group(1), match.group(2))
|
|
427
|
+
target_list.append(instance)
|
|
428
|
+
target_map[target_map_key_getter(instance)] = instance
|
|
429
|
+
elif match.group(1) == "Format" and len(target_format) == 0:
|
|
430
|
+
# Setup target format
|
|
431
|
+
for f in match.group(2).split(","):
|
|
432
|
+
target_format.append(f.strip())
|
|
433
|
+
else:
|
|
434
|
+
# Map and add
|
|
435
|
+
values = match.group(2).split(",", len(target_format) - 1)
|
|
436
|
+
instance = target_class()
|
|
437
|
+
instance.type = match.group(1)
|
|
438
|
+
|
|
439
|
+
for i in range(len(values)):
|
|
440
|
+
instance.set(target_format[i], values[i], *target_class_set_args)
|
|
441
|
+
|
|
442
|
+
target_list.append(instance)
|
|
443
|
+
if target_map is not None:
|
|
444
|
+
target_map[target_map_key_getter(instance)] = instance
|
|
445
|
+
|
|
446
|
+
# Done
|
|
447
|
+
return self
|
|
448
|
+
|
|
449
|
+
def write(self, filename, comments=None):
|
|
450
|
+
# Generate source
|
|
451
|
+
source = [
|
|
452
|
+
"[Script Info]\n",
|
|
453
|
+
]
|
|
454
|
+
|
|
455
|
+
# Comments
|
|
456
|
+
if comments is None:
|
|
457
|
+
# Default comment
|
|
458
|
+
source.extend(
|
|
459
|
+
[
|
|
460
|
+
"; Script generated by {0:s}\n".format(
|
|
461
|
+
self.__re_filename_format[0].sub(self.__re_filename_format[1], os.path.split(__file__)[1])
|
|
462
|
+
),
|
|
463
|
+
]
|
|
464
|
+
)
|
|
465
|
+
else:
|
|
466
|
+
# Custom comments
|
|
467
|
+
source.extend(["; {0:s}".format(c) for c in comments])
|
|
468
|
+
|
|
469
|
+
# Script info
|
|
470
|
+
for entry in self.script_info_ordered:
|
|
471
|
+
if entry.key in self.script_info:
|
|
472
|
+
source.append("{0:s}: {1:s}\n".format(entry.key, entry.value))
|
|
473
|
+
|
|
474
|
+
source.append("\n")
|
|
475
|
+
|
|
476
|
+
# Styles
|
|
477
|
+
source.append("[V4+ Styles]\n")
|
|
478
|
+
source.append("Format: {0:s}\n".format(", ".join(self.styles_format)))
|
|
479
|
+
for style in self.styles:
|
|
480
|
+
style_list = []
|
|
481
|
+
for key in self.styles_format:
|
|
482
|
+
style_list.append(style.get(key))
|
|
483
|
+
source.append("{0:s}: {1:s}\n".format(style.type, ",".join(style_list)))
|
|
484
|
+
source.append("\n")
|
|
485
|
+
|
|
486
|
+
# Events
|
|
487
|
+
source.append("[Events]\n")
|
|
488
|
+
source.append("Format: {0:s}\n".format(", ".join(self.events_format)))
|
|
489
|
+
for event in self.events:
|
|
490
|
+
if event.Start >= 0 and event.End >= 0:
|
|
491
|
+
event_list = []
|
|
492
|
+
for key in self.events_format:
|
|
493
|
+
event_list.append(event.get(key))
|
|
494
|
+
source.append("{0:s}: {1:s}\n".format(event.type, ",".join(event_list)))
|
|
495
|
+
|
|
496
|
+
# Write file
|
|
497
|
+
f = open(filename, "wb")
|
|
498
|
+
s = f.write(("".join(source)).encode("utf-8"))
|
|
499
|
+
f.close()
|
|
500
|
+
|
|
501
|
+
# Done
|
|
502
|
+
return self
|
|
503
|
+
|
|
504
|
+
def write_srt(self, filename, **kwargs):
|
|
505
|
+
# Parse kwargs
|
|
506
|
+
overlap = self.__kwarg_default(kwargs, "overlap", True)
|
|
507
|
+
# if True, overlapping timecodes are allowed; else, overlapping timecodes are split
|
|
508
|
+
newlines = self.__kwarg_default(kwargs, "newlines", False)
|
|
509
|
+
# if True, minimal newlines are preserved
|
|
510
|
+
remove_identical = self.__kwarg_default(kwargs, "remove_identical", True)
|
|
511
|
+
# if True, identical lines (after tags are changed/removed) are removed
|
|
512
|
+
join = self.__kwarg_default(kwargs, "join", True)
|
|
513
|
+
# if True, identical sequential lines are joined
|
|
514
|
+
filter_function = self.__kwarg_default(kwargs, "filter_function", None)
|
|
515
|
+
# custom function to filter lines: takes 2 arguments: (event, final_text) and should return the same (or modified) final_text to keep, or None to remove
|
|
516
|
+
|
|
517
|
+
# Source
|
|
518
|
+
source = []
|
|
519
|
+
|
|
520
|
+
# Events
|
|
521
|
+
sorted_events = []
|
|
522
|
+
for i in range(len(self.events)):
|
|
523
|
+
event = self.events[i]
|
|
524
|
+
if event.type == "Dialogue" and event.Start < event.End and event.Start >= 0:
|
|
525
|
+
meta_event = self.__WriteSRTMetaEvent(event, i)
|
|
526
|
+
meta_event.format_text(self, newlines)
|
|
527
|
+
if len(meta_event.text) > 0:
|
|
528
|
+
sorted_events.append(meta_event)
|
|
529
|
+
sorted_events.sort(key=lambda e: e.start, reverse=overlap)
|
|
530
|
+
# reverse if overlap is allowed, since items are .pop'd from the end
|
|
531
|
+
|
|
532
|
+
# Filter
|
|
533
|
+
event_count = len(sorted_events)
|
|
534
|
+
if remove_identical:
|
|
535
|
+
i = 0
|
|
536
|
+
while i < event_count:
|
|
537
|
+
event = sorted_events[i]
|
|
538
|
+
j = i + 1
|
|
539
|
+
while j < event_count:
|
|
540
|
+
if event.equals(sorted_events[j]):
|
|
541
|
+
# Remove
|
|
542
|
+
sorted_events.pop(j)
|
|
543
|
+
event_count -= 1
|
|
544
|
+
continue
|
|
545
|
+
elif event.start < sorted_events[j].start:
|
|
546
|
+
# Done
|
|
547
|
+
break
|
|
548
|
+
|
|
549
|
+
# Next
|
|
550
|
+
j += 1
|
|
551
|
+
|
|
552
|
+
# Next
|
|
553
|
+
i += 1
|
|
554
|
+
if filter_function is not None:
|
|
555
|
+
i = 0
|
|
556
|
+
while i < event_count:
|
|
557
|
+
result = filter_function(sorted_events[i].event, sorted_events[i].text)
|
|
558
|
+
if result is None:
|
|
559
|
+
# Remove
|
|
560
|
+
sorted_events.pop(i)
|
|
561
|
+
event_count -= 1
|
|
562
|
+
continue
|
|
563
|
+
else:
|
|
564
|
+
sorted_events[i].text = result
|
|
565
|
+
|
|
566
|
+
# Next
|
|
567
|
+
i += 1
|
|
568
|
+
|
|
569
|
+
# Format
|
|
570
|
+
lines = []
|
|
571
|
+
while event_count > 0:
|
|
572
|
+
if overlap:
|
|
573
|
+
# Simple mode; no overlap check
|
|
574
|
+
event_data = sorted_events.pop()
|
|
575
|
+
block_start = event_data.start
|
|
576
|
+
block_end = event_data.event.End
|
|
577
|
+
stack_lines = [event_data]
|
|
578
|
+
event_count -= 1
|
|
579
|
+
else:
|
|
580
|
+
# Find time block range
|
|
581
|
+
event_data = sorted_events[0]
|
|
582
|
+
block_start = event_data.start
|
|
583
|
+
block_end = event_data.event.End
|
|
584
|
+
for i in range(1, event_count):
|
|
585
|
+
event_data = sorted_events[i]
|
|
586
|
+
if event_data.start < block_start + self.__same_time_max_delta: # will set even if same
|
|
587
|
+
block_start = event_data.start
|
|
588
|
+
if event_data.event.End <= block_end - self.__same_time_max_delta: # will set only if lower
|
|
589
|
+
block_end = event_data.event.End
|
|
590
|
+
elif event_data.start <= block_end - self.__same_time_max_delta: # will set only if lower
|
|
591
|
+
block_end = event_data.start
|
|
592
|
+
assert block_start < block_end
|
|
593
|
+
# should never happen
|
|
594
|
+
|
|
595
|
+
# Discover lines
|
|
596
|
+
ac = event_count
|
|
597
|
+
i = 0
|
|
598
|
+
stack_lines = []
|
|
599
|
+
stack_lines_ordered = collections.deque()
|
|
600
|
+
stack_lines_unordered = collections.deque()
|
|
601
|
+
while i < event_count:
|
|
602
|
+
event_data = sorted_events[i]
|
|
603
|
+
if event_data.start <= block_end - self.__same_time_max_delta:
|
|
604
|
+
# This line is included
|
|
605
|
+
if event_data.y_pos >= 0:
|
|
606
|
+
stack_lines_ordered.append(event_data)
|
|
607
|
+
else:
|
|
608
|
+
stack_lines_unordered.append(event_data)
|
|
609
|
+
if event_data.event.End <= block_end + self.__same_time_max_delta:
|
|
610
|
+
# Remove
|
|
611
|
+
sorted_events.pop(i)
|
|
612
|
+
event_count -= 1
|
|
613
|
+
continue
|
|
614
|
+
else:
|
|
615
|
+
# Update start
|
|
616
|
+
sorted_events[i].start = block_end
|
|
617
|
+
|
|
618
|
+
# Next
|
|
619
|
+
i += 1
|
|
620
|
+
|
|
621
|
+
# Sort lines
|
|
622
|
+
i = 0
|
|
623
|
+
# stack_lines_ordered = collections.deque(sorted(stack_lines_ordered, key=lambda e: e[1]));
|
|
624
|
+
while len(stack_lines_ordered) > 0 and len(stack_lines_unordered) > 0:
|
|
625
|
+
if stack_lines_ordered[0].y_pos == i:
|
|
626
|
+
stack_lines.append(stack_lines_ordered.popleft())
|
|
627
|
+
found = True
|
|
628
|
+
else:
|
|
629
|
+
e = stack_lines_unordered.popleft()
|
|
630
|
+
stack_lines.append(e)
|
|
631
|
+
|
|
632
|
+
# Next
|
|
633
|
+
i += 1
|
|
634
|
+
stack_lines.extend(stack_lines_ordered)
|
|
635
|
+
if len(stack_lines_unordered) > 1:
|
|
636
|
+
# Sort by vertical position; this is convenient for multiple lines appearing simultaneously; there are still cases ordering may be messed up
|
|
637
|
+
stack_lines_unordered = sorted(
|
|
638
|
+
stack_lines_unordered,
|
|
639
|
+
key=functools.cmp_to_key(lambda e1, e2: self.__write_srt_sort_lines_compare(e1, e2)),
|
|
640
|
+
)
|
|
641
|
+
stack_lines.extend(stack_lines_unordered)
|
|
642
|
+
for i in range(len(stack_lines)):
|
|
643
|
+
stack_lines[i].y_pos = i
|
|
644
|
+
|
|
645
|
+
# Process lines
|
|
646
|
+
text = []
|
|
647
|
+
for e in reversed(stack_lines):
|
|
648
|
+
text.append(e.text)
|
|
649
|
+
|
|
650
|
+
# Add
|
|
651
|
+
lines.append([block_start, block_end, "\n".join(text)])
|
|
652
|
+
|
|
653
|
+
# Join
|
|
654
|
+
if join:
|
|
655
|
+
i = 0
|
|
656
|
+
i_max = len(lines) - 1
|
|
657
|
+
while i < i_max:
|
|
658
|
+
if lines[i][2] == lines[i + 1][2] and lines[i][1] == lines[i + 1][0]:
|
|
659
|
+
lines[i][1] = lines[i + 1][1]
|
|
660
|
+
lines.pop(i + 1)
|
|
661
|
+
i_max -= 1
|
|
662
|
+
continue
|
|
663
|
+
|
|
664
|
+
# Next
|
|
665
|
+
i += 1
|
|
666
|
+
|
|
667
|
+
# Process
|
|
668
|
+
for i in range(len(lines)):
|
|
669
|
+
line_start, line_end, line_text = lines[i]
|
|
670
|
+
|
|
671
|
+
source.append("{0:d}\n".format(i + 1))
|
|
672
|
+
source.append(
|
|
673
|
+
"{0:s} --> {1:s}\n".format(
|
|
674
|
+
Formatters.timecode_to_str_generic(line_start, 3, 2, 2, 2).replace(".", ","),
|
|
675
|
+
Formatters.timecode_to_str_generic(line_end, 3, 2, 2, 2).replace(".", ","),
|
|
676
|
+
)
|
|
677
|
+
)
|
|
678
|
+
source.append("{0:s}\n\n".format(line_text))
|
|
679
|
+
|
|
680
|
+
# Write file
|
|
681
|
+
f = open(filename, "wb")
|
|
682
|
+
s = f.write(("".join(source)).encode("utf-8"))
|
|
683
|
+
f.close()
|
|
684
|
+
|
|
685
|
+
# Done
|
|
686
|
+
return self
|
|
687
|
+
|
|
688
|
+
def __write_srt_sort_lines_compare(self, line1, line2):
|
|
689
|
+
# Sort by position
|
|
690
|
+
order = 1
|
|
691
|
+
pos1 = self.__get_line_position(line1.event)
|
|
692
|
+
pos2 = self.__get_line_position(line2.event)
|
|
693
|
+
if pos1 is not None and pos2 is not None:
|
|
694
|
+
if pos1[1] > pos2[1]:
|
|
695
|
+
return -order
|
|
696
|
+
if pos1[1] < pos2[1]:
|
|
697
|
+
return order
|
|
698
|
+
|
|
699
|
+
# Sort by vertical alignment
|
|
700
|
+
align1_y = self.get_xy_alignment(self.get_line_alignment(line1.event, True))[1]
|
|
701
|
+
align2_y = self.get_xy_alignment(self.get_line_alignment(line2.event, True))[1]
|
|
702
|
+
|
|
703
|
+
if align1_y > align2_y:
|
|
704
|
+
return -order
|
|
705
|
+
if align1_y < align2_y:
|
|
706
|
+
return order
|
|
707
|
+
|
|
708
|
+
if align1_y < 0:
|
|
709
|
+
order = -order
|
|
710
|
+
# switch
|
|
711
|
+
|
|
712
|
+
# Sort by vertical margin
|
|
713
|
+
margin1 = line1.event.MarginV
|
|
714
|
+
margin2 = line2.event.MarginV
|
|
715
|
+
if margin1 == 0:
|
|
716
|
+
margin1 = line1.event.Style.MarginV
|
|
717
|
+
if margin2 == 0:
|
|
718
|
+
margin2 = line2.event.Style.MarginV
|
|
719
|
+
|
|
720
|
+
if margin1 < margin2:
|
|
721
|
+
return -order
|
|
722
|
+
if margin1 > margin2:
|
|
723
|
+
return order
|
|
724
|
+
|
|
725
|
+
# Sort by order of appearance
|
|
726
|
+
if line1.index < line2.index:
|
|
727
|
+
return -order
|
|
728
|
+
if line1.index > line2.index:
|
|
729
|
+
return order
|
|
730
|
+
return 0
|
|
731
|
+
|
|
732
|
+
class __WriteSRTMetaEvent:
|
|
733
|
+
def __init__(self, event, i):
|
|
734
|
+
self.event = event
|
|
735
|
+
self.start = event.Start
|
|
736
|
+
self.y_pos = -1
|
|
737
|
+
self.index = i
|
|
738
|
+
self.text = None
|
|
739
|
+
|
|
740
|
+
def equals(self, other):
|
|
741
|
+
return self.text == other.text and self.start == other.start and self.event.End == other.event.End
|
|
742
|
+
|
|
743
|
+
def format_text(self, parent, newlines):
|
|
744
|
+
self.text = parent.parse_text(
|
|
745
|
+
self.event.Text,
|
|
746
|
+
modify_text=(lambda t: self.__write_srt_format_text(parent, newlines, t)),
|
|
747
|
+
modify_tag_block=(lambda b: ""),
|
|
748
|
+
modify_geometry=(lambda g: ""),
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
def __write_srt_format_text(self, parent, newlines, text):
|
|
752
|
+
return parent.replace_special(text, (lambda c: self.__write_srt_format_text_space(newlines, c)), 1, 1)
|
|
753
|
+
|
|
754
|
+
def __write_srt_format_text_space(self, newlines, character):
|
|
755
|
+
if character == "h":
|
|
756
|
+
return "\u00A0"
|
|
757
|
+
if newlines:
|
|
758
|
+
return "\n"
|
|
759
|
+
return " "
|
|
760
|
+
|
|
761
|
+
def set_resolution(self, resolution):
|
|
762
|
+
self.__set_script_info("PlayResX", str(resolution[0]))
|
|
763
|
+
self.__set_script_info("PlayResY", str(resolution[1]))
|
|
764
|
+
|
|
765
|
+
# Script resolution
|
|
766
|
+
def resolution(self):
|
|
767
|
+
w = 0
|
|
768
|
+
h = 0
|
|
769
|
+
if "PlayResX" in self.script_info:
|
|
770
|
+
try:
|
|
771
|
+
w = int(self.script_info["PlayResX"].value, 10)
|
|
772
|
+
except ValueError:
|
|
773
|
+
pass
|
|
774
|
+
if "PlayResY" in self.script_info:
|
|
775
|
+
try:
|
|
776
|
+
h = int(self.script_info["PlayResY"].value, 10)
|
|
777
|
+
except ValueError:
|
|
778
|
+
pass
|
|
779
|
+
|
|
780
|
+
return (w, h)
|
|
781
|
+
|
|
782
|
+
# Alignment
|
|
783
|
+
@classmethod
|
|
784
|
+
def get_line_alignment(cls, event, deep=True):
|
|
785
|
+
state = [None]
|
|
786
|
+
|
|
787
|
+
# Check more
|
|
788
|
+
if deep:
|
|
789
|
+
cls.parse_text(
|
|
790
|
+
event.Text,
|
|
791
|
+
modify_tag=(lambda t: cls.__get_line_alignment_modify_tag(state, t)),
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
# Return
|
|
795
|
+
if state[0] is None:
|
|
796
|
+
state[0] = event.Style.Alignment
|
|
797
|
+
return state[0]
|
|
798
|
+
|
|
799
|
+
@classmethod
|
|
800
|
+
def __get_line_alignment_modify_tag(cls, state, tag):
|
|
801
|
+
if state[0] is None:
|
|
802
|
+
tag_name = tag[0]
|
|
803
|
+
if tag_name == "a":
|
|
804
|
+
state[0] = cls.__legacy_align_to_regular(Formatters.str_to_number(tag[1]))
|
|
805
|
+
elif tag_name == "an":
|
|
806
|
+
state[0] = Formatters.str_to_number(tag[1])
|
|
807
|
+
|
|
808
|
+
# Done
|
|
809
|
+
return [tag]
|
|
810
|
+
|
|
811
|
+
@classmethod
|
|
812
|
+
def get_xy_alignment(cls, align):
|
|
813
|
+
if align >= ALIGN_TOP_LEFT and align <= ALIGN_TOP_RIGHT:
|
|
814
|
+
align_y = -1
|
|
815
|
+
if align == ALIGN_TOP_LEFT:
|
|
816
|
+
align_x = -1
|
|
817
|
+
elif align == ALIGN_TOP_RIGHT:
|
|
818
|
+
align_x = 1
|
|
819
|
+
else:
|
|
820
|
+
align_x = 0
|
|
821
|
+
elif align >= ALIGN_MIDDLE_LEFT and align <= ALIGN_MIDDLE_RIGHT:
|
|
822
|
+
align_y = 0
|
|
823
|
+
if align == ALIGN_MIDDLE_LEFT:
|
|
824
|
+
align_x = -1
|
|
825
|
+
elif align == ALIGN_MIDDLE_RIGHT:
|
|
826
|
+
align_x = 1
|
|
827
|
+
else:
|
|
828
|
+
align_x = 0
|
|
829
|
+
else: # if (align >= ALIGN_BOTTOM_LEFT and align <= ALIGN_BOTTOM_RIGHT):
|
|
830
|
+
align_y = 1
|
|
831
|
+
if align == ALIGN_BOTTOM_LEFT:
|
|
832
|
+
align_x = -1
|
|
833
|
+
elif align == ALIGN_BOTTOM_RIGHT:
|
|
834
|
+
align_x = 1
|
|
835
|
+
else:
|
|
836
|
+
align_x = 0
|
|
837
|
+
|
|
838
|
+
return (align_x, align_y)
|
|
839
|
+
|
|
840
|
+
@classmethod
|
|
841
|
+
def __get_line_position(cls, event):
|
|
842
|
+
state = [None]
|
|
843
|
+
|
|
844
|
+
# Check more
|
|
845
|
+
cls.parse_text(
|
|
846
|
+
event.Text,
|
|
847
|
+
modify_tag=(lambda t: cls.__get_line_position_modify_tag(state, t)),
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
# Return
|
|
851
|
+
return state[0]
|
|
852
|
+
|
|
853
|
+
@classmethod
|
|
854
|
+
def __get_line_position_modify_tag(cls, state, tag):
|
|
855
|
+
if state[0] is None:
|
|
856
|
+
tag_name = tag[0]
|
|
857
|
+
if tag_name == "pos":
|
|
858
|
+
try:
|
|
859
|
+
state[0] = (float(tag[1]), float(tag[2]))
|
|
860
|
+
except ValueError:
|
|
861
|
+
pass
|
|
862
|
+
|
|
863
|
+
# Done
|
|
864
|
+
return [tag]
|
|
865
|
+
|
|
866
|
+
# Line parsing
|
|
867
|
+
@classmethod
|
|
868
|
+
def parse_text(
|
|
869
|
+
cls,
|
|
870
|
+
text,
|
|
871
|
+
modify_text=None,
|
|
872
|
+
modify_special=None,
|
|
873
|
+
modify_tag_block=None,
|
|
874
|
+
modify_tag=None,
|
|
875
|
+
modify_comment=None,
|
|
876
|
+
modify_geometry=None,
|
|
877
|
+
):
|
|
878
|
+
"""
|
|
879
|
+
modify_tag:
|
|
880
|
+
inputs:
|
|
881
|
+
tag_args - an array of the form:
|
|
882
|
+
[ tag_name , tag_arg1 , tag_arg2 , ... ]
|
|
883
|
+
where all tag_arg#'s are optional
|
|
884
|
+
return:
|
|
885
|
+
must return an array containing only "tag_args" and strings
|
|
886
|
+
- "tag_args" are auto-converted into strings
|
|
887
|
+
- strings are treated as comments, or pre-formatted tags
|
|
888
|
+
|
|
889
|
+
<everything else>:
|
|
890
|
+
inputs:
|
|
891
|
+
the relevant string
|
|
892
|
+
return:
|
|
893
|
+
the relevant string, modified
|
|
894
|
+
|
|
895
|
+
Note:
|
|
896
|
+
if modify_special is None, then "\\h", "\\n", and "\\N" will be treated part of text sections (i.e. they are not separated)
|
|
897
|
+
"""
|
|
898
|
+
text_new = []
|
|
899
|
+
|
|
900
|
+
if modify_special is None:
|
|
901
|
+
re_matcher = cls.__re_tag_block
|
|
902
|
+
else:
|
|
903
|
+
re_matcher = cls.__re_tag_block_or_special
|
|
904
|
+
|
|
905
|
+
next_geometry_scale = 0
|
|
906
|
+
pos = 0
|
|
907
|
+
|
|
908
|
+
for match in re_matcher.finditer(text):
|
|
909
|
+
# Previous text
|
|
910
|
+
if match.start(0) > pos:
|
|
911
|
+
t = text[pos : match.start(0)]
|
|
912
|
+
if next_geometry_scale <= 0:
|
|
913
|
+
if modify_text is not None:
|
|
914
|
+
t = modify_text(t)
|
|
915
|
+
else:
|
|
916
|
+
if modify_geometry is not None:
|
|
917
|
+
t = modify_geometry(t)
|
|
918
|
+
|
|
919
|
+
text_new.append(t)
|
|
920
|
+
|
|
921
|
+
# Tag block
|
|
922
|
+
if match.group(2) is None:
|
|
923
|
+
t = match.group(4)
|
|
924
|
+
t = modify_special(t)
|
|
925
|
+
text_new.append(t)
|
|
926
|
+
else:
|
|
927
|
+
tag_new = [match.group(1)]
|
|
928
|
+
|
|
929
|
+
# Parse individual tags
|
|
930
|
+
tag_text, next_geometry_scale = cls.parse_tags(match.group(2), modify_tag, modify_comment, next_geometry_scale)
|
|
931
|
+
tag_text = match.group(1) + tag_text + match.group(3)
|
|
932
|
+
|
|
933
|
+
if modify_tag_block is not None:
|
|
934
|
+
tag_text = modify_tag_block(tag_text)
|
|
935
|
+
|
|
936
|
+
text_new.append(tag_text)
|
|
937
|
+
|
|
938
|
+
# Next
|
|
939
|
+
pos = match.end(0)
|
|
940
|
+
|
|
941
|
+
# Final
|
|
942
|
+
if pos < len(text):
|
|
943
|
+
t = text[pos:]
|
|
944
|
+
if next_geometry_scale <= 0:
|
|
945
|
+
if modify_text is not None:
|
|
946
|
+
t = modify_text(t)
|
|
947
|
+
else:
|
|
948
|
+
if modify_geometry is not None:
|
|
949
|
+
t = modify_geometry(t)
|
|
950
|
+
|
|
951
|
+
text_new.append(t)
|
|
952
|
+
|
|
953
|
+
# Done
|
|
954
|
+
return "".join(text_new)
|
|
955
|
+
|
|
956
|
+
@classmethod
|
|
957
|
+
def parse_tags(cls, text, modify_tag=None, modify_comment=None, next_geometry_scale=0):
|
|
958
|
+
"""
|
|
959
|
+
modify_tag:
|
|
960
|
+
inputs:
|
|
961
|
+
tag_args - an array of the form:
|
|
962
|
+
[ tag_name , tag_arg1 , tag_arg2 , ... ]
|
|
963
|
+
where all tag_arg#'s are optional
|
|
964
|
+
return:
|
|
965
|
+
must return an array containing only "tag_args" and strings
|
|
966
|
+
- "tag_args" are auto-converted into strings
|
|
967
|
+
- strings are treated as comments, or pre-formatted tags
|
|
968
|
+
|
|
969
|
+
<everything else>:
|
|
970
|
+
inputs:
|
|
971
|
+
the relevant string
|
|
972
|
+
return:
|
|
973
|
+
the relevant string, modified
|
|
974
|
+
"""
|
|
975
|
+
text_new = []
|
|
976
|
+
pos = 0
|
|
977
|
+
for match in cls.__re_tag.finditer(text):
|
|
978
|
+
# Comment
|
|
979
|
+
if match.start(0) > pos:
|
|
980
|
+
tt = text[pos : match.start(0)]
|
|
981
|
+
if modify_comment is not None:
|
|
982
|
+
tt = modify_comment(tt)
|
|
983
|
+
text_new.append(tt)
|
|
984
|
+
|
|
985
|
+
# Tag
|
|
986
|
+
tt = match.group(0)
|
|
987
|
+
tg = match.groups()
|
|
988
|
+
|
|
989
|
+
start = 0
|
|
990
|
+
while tg[start] is None:
|
|
991
|
+
start += 1
|
|
992
|
+
end = start + 1
|
|
993
|
+
while tg[end] is not None:
|
|
994
|
+
end += 1
|
|
995
|
+
|
|
996
|
+
tag_args = tg[start:end]
|
|
997
|
+
|
|
998
|
+
if modify_tag is None:
|
|
999
|
+
tag_args_array = [tag_args]
|
|
1000
|
+
else:
|
|
1001
|
+
tag_args_array = modify_tag(tag_args)
|
|
1002
|
+
|
|
1003
|
+
# Convert to a string
|
|
1004
|
+
tt_array = []
|
|
1005
|
+
for tag_args in tag_args_array:
|
|
1006
|
+
if cls.__py_2or3_var_is_string(tag_args):
|
|
1007
|
+
tt = tag_args
|
|
1008
|
+
else:
|
|
1009
|
+
if tag_args[0] in cls.__tags_with_parentheses:
|
|
1010
|
+
tt = "\\{0:s}({1:s})"
|
|
1011
|
+
else:
|
|
1012
|
+
tt = "\\{0:s}{1:s}"
|
|
1013
|
+
tt = tt.format(tag_args[0], ",".join(tag_args[1:]))
|
|
1014
|
+
tt_array.append(tt)
|
|
1015
|
+
tt = "".join(tt_array)
|
|
1016
|
+
|
|
1017
|
+
for tag_args in tag_args_array:
|
|
1018
|
+
if tag_args[0] == "p":
|
|
1019
|
+
# Drawing command
|
|
1020
|
+
next_geometry_scale = Formatters.tag_argument_to_number(tag_args[1], 0)
|
|
1021
|
+
|
|
1022
|
+
text_new.append(tt)
|
|
1023
|
+
|
|
1024
|
+
# Next
|
|
1025
|
+
pos = match.end(0)
|
|
1026
|
+
|
|
1027
|
+
# Final comment
|
|
1028
|
+
if pos < len(text):
|
|
1029
|
+
tt = text[pos:]
|
|
1030
|
+
if modify_comment is not None:
|
|
1031
|
+
tt = modify_comment(tt)
|
|
1032
|
+
text_new.append(tt)
|
|
1033
|
+
|
|
1034
|
+
# Done
|
|
1035
|
+
return ("".join(text_new), next_geometry_scale)
|
|
1036
|
+
|
|
1037
|
+
# Other parsing
|
|
1038
|
+
@classmethod
|
|
1039
|
+
def replace_special(cls, text, space=" ", min_whitespace_length=1, max_whitespace_length=1):
|
|
1040
|
+
return cls.__re_remove_special.sub(
|
|
1041
|
+
(lambda m: cls.__replace_special_replacer(m, space, min_whitespace_length, max_whitespace_length)),
|
|
1042
|
+
text,
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
@classmethod
|
|
1046
|
+
def __replace_special_replacer(cls, match, space, min_whitespace_length, max_whitespace_length):
|
|
1047
|
+
ws = match.group(1) + match.group(3)
|
|
1048
|
+
ws_len = len(ws)
|
|
1049
|
+
|
|
1050
|
+
if ws_len < min_whitespace_length or (ws_len > max_whitespace_length and max_whitespace_length >= 0):
|
|
1051
|
+
if hasattr(space, "__call__"):
|
|
1052
|
+
return space(match.group(2))
|
|
1053
|
+
return space
|
|
1054
|
+
|
|
1055
|
+
return ws
|
|
1056
|
+
|
|
1057
|
+
# Regenerate format orders
|
|
1058
|
+
def reformat(self, **kwargs):
|
|
1059
|
+
# Parse kwargs
|
|
1060
|
+
alias = self.__kwarg_default(kwargs, "alias", False)
|
|
1061
|
+
# doesn't do anything since there aren't aliases for events; kept for consistenccy
|
|
1062
|
+
|
|
1063
|
+
# Process
|
|
1064
|
+
main_cls = self.Event
|
|
1065
|
+
new_format = list(main_cls.order)
|
|
1066
|
+
|
|
1067
|
+
# Alias
|
|
1068
|
+
if alias:
|
|
1069
|
+
for i in range(len(new_format)):
|
|
1070
|
+
attr_name = new_format[i]
|
|
1071
|
+
if attr_name in main_cls.aliases:
|
|
1072
|
+
new_format[i] = main_cls.aliases[attr_name]
|
|
1073
|
+
|
|
1074
|
+
# Apply
|
|
1075
|
+
self.events_format = new_format
|
|
1076
|
+
|
|
1077
|
+
# Done
|
|
1078
|
+
return self
|
|
1079
|
+
|
|
1080
|
+
def reformat_styles(self, **kwargs):
|
|
1081
|
+
# Parse kwargs
|
|
1082
|
+
alias = self.__kwarg_default(kwargs, "alias", False)
|
|
1083
|
+
# if False, British spellings of "colour" are used (.ass files seem to function either way)
|
|
1084
|
+
|
|
1085
|
+
# Process
|
|
1086
|
+
main_cls = self.Style
|
|
1087
|
+
new_format = list(main_cls.order)
|
|
1088
|
+
|
|
1089
|
+
# Alias
|
|
1090
|
+
if alias:
|
|
1091
|
+
for i in range(len(new_format)):
|
|
1092
|
+
attr_name = new_format[i]
|
|
1093
|
+
if attr_name in main_cls.aliases:
|
|
1094
|
+
new_format[i] = main_cls.aliases[attr_name]
|
|
1095
|
+
|
|
1096
|
+
# Apply
|
|
1097
|
+
self.styles_format = new_format
|
|
1098
|
+
|
|
1099
|
+
# Done
|
|
1100
|
+
return self
|
|
1101
|
+
|
|
1102
|
+
# Add events/styles
|
|
1103
|
+
def add(self, event):
|
|
1104
|
+
self.events.append(event)
|
|
1105
|
+
|
|
1106
|
+
# Check if a new style is necessary
|
|
1107
|
+
if not event.Style.fake:
|
|
1108
|
+
same_style = None
|
|
1109
|
+
for style in self.styles:
|
|
1110
|
+
if event.Style is style:
|
|
1111
|
+
# Already exists
|
|
1112
|
+
return
|
|
1113
|
+
elif event.Style.equals(style):
|
|
1114
|
+
# Already exists
|
|
1115
|
+
same_style = style
|
|
1116
|
+
|
|
1117
|
+
if same_style is not None:
|
|
1118
|
+
# Copy
|
|
1119
|
+
event.Style = same_style
|
|
1120
|
+
else:
|
|
1121
|
+
# Add a new style
|
|
1122
|
+
event.Style = event.Style.copy()
|
|
1123
|
+
self.add_style(event.Style)
|
|
1124
|
+
|
|
1125
|
+
# Done
|
|
1126
|
+
return self
|
|
1127
|
+
|
|
1128
|
+
def add_style(self, style):
|
|
1129
|
+
self.styles.append(style)
|
|
1130
|
+
|
|
1131
|
+
# Done
|
|
1132
|
+
return self
|
|
1133
|
+
|
|
1134
|
+
# Tidy modifications
|
|
1135
|
+
def tidy(self, **kwargs): # Join duplicates, sort
|
|
1136
|
+
# Parse kwargs
|
|
1137
|
+
sort = self.__kwarg_default(kwargs, "sort", False)
|
|
1138
|
+
# if True, events are sorted by starting time
|
|
1139
|
+
join = self.__kwarg_default(kwargs, "join", False)
|
|
1140
|
+
# if True, sequential events that would be visible as one are joined
|
|
1141
|
+
join_naive = self.__kwarg_default(kwargs, "join_naive", False)
|
|
1142
|
+
# if True, line joining will ignore any animation tags and join them anyway
|
|
1143
|
+
remove_unseen = self.__kwarg_default(kwargs, "remove_unseen", True)
|
|
1144
|
+
# if True, events with a duration of 0 (or less) are removed
|
|
1145
|
+
snap_start = self.__kwarg_default(kwargs, "snap_start", 0.0)
|
|
1146
|
+
# if greater than 0, starting timecodes within the specified time will be snapped together
|
|
1147
|
+
snap_end = self.__kwarg_default(kwargs, "snap_end", 0.0)
|
|
1148
|
+
# if greater than 0, ending timecodes within the specified time will be snapped together
|
|
1149
|
+
snap_together = self.__kwarg_default(kwargs, "snap_together", 0.0)
|
|
1150
|
+
# if greater than 0, start/end or end/start timecodes within the specified time will be snapped together
|
|
1151
|
+
|
|
1152
|
+
# Snap
|
|
1153
|
+
if snap_start > 0:
|
|
1154
|
+
for i in range(len(self.events)):
|
|
1155
|
+
e1 = self.events[i]
|
|
1156
|
+
for j in range(i + 1, len(self.events)):
|
|
1157
|
+
e2 = self.events[j]
|
|
1158
|
+
if abs(e1.Start - e2.Start) <= snap_start:
|
|
1159
|
+
# Perform snap
|
|
1160
|
+
e2.Start = e1.Start
|
|
1161
|
+
|
|
1162
|
+
if snap_end > 0:
|
|
1163
|
+
for i in range(len(self.events)):
|
|
1164
|
+
e1 = self.events[i]
|
|
1165
|
+
for j in range(i + 1, len(self.events)):
|
|
1166
|
+
e2 = self.events[j]
|
|
1167
|
+
if abs(e1.End - e2.End) <= snap_end:
|
|
1168
|
+
# Perform snap
|
|
1169
|
+
e2.End = e1.End
|
|
1170
|
+
|
|
1171
|
+
if snap_together > 0:
|
|
1172
|
+
for i in range(len(self.events)):
|
|
1173
|
+
e1 = self.events[i]
|
|
1174
|
+
for j in range(i + 1, len(self.events)):
|
|
1175
|
+
e2 = self.events[j]
|
|
1176
|
+
if abs(e1.Start - e2.End) <= snap_together:
|
|
1177
|
+
# Perform snap
|
|
1178
|
+
e2.End = e1.Start
|
|
1179
|
+
if abs(e1.End - e2.Start) <= snap_together:
|
|
1180
|
+
# Perform snap
|
|
1181
|
+
e2.Start = e1.End
|
|
1182
|
+
|
|
1183
|
+
# Join
|
|
1184
|
+
if join:
|
|
1185
|
+
i = 0
|
|
1186
|
+
events_len = len(self.events)
|
|
1187
|
+
while i < events_len:
|
|
1188
|
+
e1 = self.events[i]
|
|
1189
|
+
|
|
1190
|
+
j = 0
|
|
1191
|
+
while j < events_len:
|
|
1192
|
+
if j != i:
|
|
1193
|
+
# Styles match
|
|
1194
|
+
e2 = self.events[j]
|
|
1195
|
+
if e1.same_style(e2) and e1.type == e2.type:
|
|
1196
|
+
# Attempt join
|
|
1197
|
+
e_joined = self.__join_lines(e1, e2, join_naive)
|
|
1198
|
+
if e_joined is not None:
|
|
1199
|
+
# Update
|
|
1200
|
+
e1 = e_joined
|
|
1201
|
+
self.events[i] = e1
|
|
1202
|
+
events_len -= 1
|
|
1203
|
+
|
|
1204
|
+
# Remove
|
|
1205
|
+
self.events.pop(j)
|
|
1206
|
+
if i > j:
|
|
1207
|
+
i -= 1
|
|
1208
|
+
|
|
1209
|
+
# Reset loop
|
|
1210
|
+
j = 0
|
|
1211
|
+
continue
|
|
1212
|
+
|
|
1213
|
+
# Next
|
|
1214
|
+
j += 1
|
|
1215
|
+
|
|
1216
|
+
# Next
|
|
1217
|
+
i += 1
|
|
1218
|
+
|
|
1219
|
+
# Sort
|
|
1220
|
+
if sort:
|
|
1221
|
+
self.events.sort(key=lambda e: e.Start)
|
|
1222
|
+
|
|
1223
|
+
# Remove 0 length
|
|
1224
|
+
if remove_unseen:
|
|
1225
|
+
i = 0
|
|
1226
|
+
i_max = len(self.events)
|
|
1227
|
+
while i < i_max:
|
|
1228
|
+
e = self.events[i]
|
|
1229
|
+
if e.End - e.Start <= 0:
|
|
1230
|
+
self.events.pop(i)
|
|
1231
|
+
i_max -= 1
|
|
1232
|
+
continue
|
|
1233
|
+
|
|
1234
|
+
# Next
|
|
1235
|
+
i += 1
|
|
1236
|
+
|
|
1237
|
+
# Done
|
|
1238
|
+
return self
|
|
1239
|
+
|
|
1240
|
+
def tidy_styles(self, **kwargs): # Generate unique names, remove duplicates, and remove unused
|
|
1241
|
+
# Parse kwargs
|
|
1242
|
+
sort = self.__kwarg_default(kwargs, "sort", False)
|
|
1243
|
+
# if True, events are sorted by name
|
|
1244
|
+
join = self.__kwarg_default(kwargs, "join", False)
|
|
1245
|
+
# if True, duplicates are joined into a single style
|
|
1246
|
+
join_if_names_differ = self.__kwarg_default(kwargs, "join_if_names_differ", False)
|
|
1247
|
+
# if True, styles are joined even if their names are different
|
|
1248
|
+
rename = self.__kwarg_default(kwargs, "rename", False)
|
|
1249
|
+
# if True, styles with identical names are renamed
|
|
1250
|
+
rename_function = self.__kwarg_default(kwargs, "rename_function", None)
|
|
1251
|
+
# if not None, then this is a function deciding the new name; format is rename_function(style_name, copy_index); it is only called on duplicate named styles; copy_index starts at 0
|
|
1252
|
+
remove_unused = self.__kwarg_default(kwargs, "remove_unused", False)
|
|
1253
|
+
# if True, unused styles are removed
|
|
1254
|
+
|
|
1255
|
+
# Setup
|
|
1256
|
+
styles_len = len(self.styles)
|
|
1257
|
+
|
|
1258
|
+
# Join
|
|
1259
|
+
if join:
|
|
1260
|
+
i = 0
|
|
1261
|
+
while i < styles_len:
|
|
1262
|
+
s1 = self.styles[i]
|
|
1263
|
+
|
|
1264
|
+
j = i + 1
|
|
1265
|
+
while j < styles_len:
|
|
1266
|
+
s2 = self.styles[j]
|
|
1267
|
+
if s1.equals(s2, join_if_names_differ):
|
|
1268
|
+
# Join
|
|
1269
|
+
self.__change_event_styles(s2, s1)
|
|
1270
|
+
self.styles.pop(j)
|
|
1271
|
+
styles_len -= 1
|
|
1272
|
+
continue
|
|
1273
|
+
|
|
1274
|
+
# Next
|
|
1275
|
+
j += 1
|
|
1276
|
+
# Next
|
|
1277
|
+
i += 1
|
|
1278
|
+
|
|
1279
|
+
# Remove unused
|
|
1280
|
+
if remove_unused:
|
|
1281
|
+
i = 0
|
|
1282
|
+
while i < styles_len:
|
|
1283
|
+
s1 = self.styles[i]
|
|
1284
|
+
|
|
1285
|
+
# Count uses
|
|
1286
|
+
count = 0
|
|
1287
|
+
for event in self.events:
|
|
1288
|
+
if event.Style is s1:
|
|
1289
|
+
count += 1
|
|
1290
|
+
|
|
1291
|
+
# Remove
|
|
1292
|
+
if count == 0:
|
|
1293
|
+
self.styles.pop(i)
|
|
1294
|
+
styles_len -= 1
|
|
1295
|
+
continue
|
|
1296
|
+
|
|
1297
|
+
# Next
|
|
1298
|
+
i += 1
|
|
1299
|
+
|
|
1300
|
+
# Rename
|
|
1301
|
+
if rename:
|
|
1302
|
+
if rename_function is None:
|
|
1303
|
+
rename_function = lambda n, i: "{0:s} ({1:d})".format(n, i + 1)
|
|
1304
|
+
|
|
1305
|
+
# Sort by name
|
|
1306
|
+
name_map = {}
|
|
1307
|
+
for style in self.styles:
|
|
1308
|
+
if style.Name in name_map:
|
|
1309
|
+
name_map[style.Name].append(style)
|
|
1310
|
+
else:
|
|
1311
|
+
name_map[style.Name] = [style]
|
|
1312
|
+
|
|
1313
|
+
# Check for duplicates
|
|
1314
|
+
for style_name, styles_list in name_map.items():
|
|
1315
|
+
if len(styles_list) > 1:
|
|
1316
|
+
# Rename duplicates
|
|
1317
|
+
for i in range(len(styles_list)):
|
|
1318
|
+
styles_list[i].Name = rename_function(style_name, i)
|
|
1319
|
+
|
|
1320
|
+
# Sort
|
|
1321
|
+
if sort:
|
|
1322
|
+
self.styles.sort(key=lambda e: e.Name)
|
|
1323
|
+
|
|
1324
|
+
# Done
|
|
1325
|
+
return self
|
|
1326
|
+
|
|
1327
|
+
# Modifications
|
|
1328
|
+
def shiftscale(self, **kwargs): # Shift/scale a section's geometry and/or timecodes
|
|
1329
|
+
# Parse kwargs
|
|
1330
|
+
start = self.__kwarg_default(kwargs, "start", None)
|
|
1331
|
+
# time to start at, or None for not bounded
|
|
1332
|
+
end = self.__kwarg_default(kwargs, "end", None)
|
|
1333
|
+
# time to start at, or None for not bounded
|
|
1334
|
+
|
|
1335
|
+
full_inclusion = self.__kwarg_default(kwargs, "full_inclusion", False)
|
|
1336
|
+
# if True, line timecodes must be fully included within the specified range
|
|
1337
|
+
inverse = self.__kwarg_default(kwargs, "inverse", False)
|
|
1338
|
+
# if True, operation is performed on all lines not included in the timecode range
|
|
1339
|
+
split = self.__kwarg_default(kwargs, "split", False)
|
|
1340
|
+
# if True, splits lines if they are not fully in the timecode range
|
|
1341
|
+
split_naive = self.__kwarg_default(kwargs, "split_naive", False)
|
|
1342
|
+
# if True, line splitting will not modify any formatting tags
|
|
1343
|
+
|
|
1344
|
+
filter_types = self.__kwarg_default(kwargs, "filter_types", None)
|
|
1345
|
+
# list of event types to include; can be anything supporting the "in" operator; None means no filtering
|
|
1346
|
+
|
|
1347
|
+
time_scale = self.__kwarg_default(kwargs, "time_scale", 1.0)
|
|
1348
|
+
# scale timecodes by this factor
|
|
1349
|
+
time_scale_origin = self.__kwarg_default(kwargs, "time_scale_origin", 0.0)
|
|
1350
|
+
# timecode scaling origin
|
|
1351
|
+
time_offset = self.__kwarg_default(kwargs, "time_offset", 0.0)
|
|
1352
|
+
# seconds to offset timecodes by
|
|
1353
|
+
time_clip_start = self.__kwarg_default(kwargs, "time_clip_start", None)
|
|
1354
|
+
# time to clip by; None = ignore; if times are shifted/scaled outside this range, they are removed/truncated as necessary; if inverse=True, this is ignored
|
|
1355
|
+
time_clip_end = self.__kwarg_default(kwargs, "time_clip_end", None)
|
|
1356
|
+
# time to clip by; None = ignore; if times are shifted/scaled outside this range, they are removed/truncated as necessary; if inverse=True, this is ignored
|
|
1357
|
+
geometry_resolution = self.__kwarg_default(kwargs, "geometry_resolution", None)
|
|
1358
|
+
# (x,y) new total resolution
|
|
1359
|
+
geometry_scale = self.__kwarg_default(kwargs, "geometry_scale", None)
|
|
1360
|
+
# (x,y) factors by which to scale geometry
|
|
1361
|
+
geometry_scale_origin = self.__kwarg_default(kwargs, "geometry_scale_origin", (0.0, 0.0))
|
|
1362
|
+
# (x,y) geometry scaling origin
|
|
1363
|
+
geometry_offset = self.__kwarg_default(kwargs, "geometry_offset", (0.0, 0.0))
|
|
1364
|
+
# (x,y) geometry shifting offset
|
|
1365
|
+
geometry_new_styles = self.__kwarg_default(kwargs, "geometry_new_styles", True)
|
|
1366
|
+
# True if new styles should be generated
|
|
1367
|
+
|
|
1368
|
+
# Exceptions
|
|
1369
|
+
if start is not None and end is not None and start > end:
|
|
1370
|
+
raise ValueError("start cannot be greater than end")
|
|
1371
|
+
|
|
1372
|
+
# Split
|
|
1373
|
+
if split:
|
|
1374
|
+
self.__range_cut(filter_types, start, end, split_naive)
|
|
1375
|
+
|
|
1376
|
+
# Time scale
|
|
1377
|
+
if time_scale != 1.0 or time_offset != 0.0:
|
|
1378
|
+
self.__range_action(
|
|
1379
|
+
filter_types,
|
|
1380
|
+
start,
|
|
1381
|
+
end,
|
|
1382
|
+
full_inclusion,
|
|
1383
|
+
inverse,
|
|
1384
|
+
(
|
|
1385
|
+
lambda line: self.__shiftscale_action_time(
|
|
1386
|
+
inverse,
|
|
1387
|
+
split_naive,
|
|
1388
|
+
time_scale,
|
|
1389
|
+
time_scale_origin,
|
|
1390
|
+
time_offset,
|
|
1391
|
+
time_clip_start,
|
|
1392
|
+
time_clip_end,
|
|
1393
|
+
line,
|
|
1394
|
+
)
|
|
1395
|
+
),
|
|
1396
|
+
)
|
|
1397
|
+
|
|
1398
|
+
# Update resolution
|
|
1399
|
+
resolution_old = self.resolution()
|
|
1400
|
+
if geometry_resolution is not None:
|
|
1401
|
+
self.__set_script_info("PlayResX", str(geometry_resolution[0]))
|
|
1402
|
+
self.__set_script_info("PlayResY", str(geometry_resolution[1]))
|
|
1403
|
+
|
|
1404
|
+
resolution_new = geometry_resolution
|
|
1405
|
+
if geometry_scale is None:
|
|
1406
|
+
geometry_scale = (
|
|
1407
|
+
geometry_resolution[0] / float(resolution_old[0]),
|
|
1408
|
+
geometry_resolution[1] / float(resolution_old[1]),
|
|
1409
|
+
)
|
|
1410
|
+
else:
|
|
1411
|
+
resolution_new = resolution_old
|
|
1412
|
+
if geometry_scale is None:
|
|
1413
|
+
geometry_scale = (1.0, 1.0)
|
|
1414
|
+
|
|
1415
|
+
# Geometry scale
|
|
1416
|
+
if (
|
|
1417
|
+
(geometry_resolution is not None)
|
|
1418
|
+
or (geometry_scale[0] != 1.0 or geometry_scale[1] != 1.0)
|
|
1419
|
+
or (geometry_offset[0] != 0.0 or geometry_offset[1] != 0.0)
|
|
1420
|
+
):
|
|
1421
|
+
# New bounds
|
|
1422
|
+
bounds = self.__shiftscale_action_get_new_bounds(
|
|
1423
|
+
geometry_scale,
|
|
1424
|
+
geometry_scale_origin,
|
|
1425
|
+
geometry_offset,
|
|
1426
|
+
resolution_old,
|
|
1427
|
+
resolution_new,
|
|
1428
|
+
)
|
|
1429
|
+
|
|
1430
|
+
# Modify
|
|
1431
|
+
used_styles = {}
|
|
1432
|
+
self.__range_action(
|
|
1433
|
+
filter_types,
|
|
1434
|
+
start,
|
|
1435
|
+
end,
|
|
1436
|
+
full_inclusion,
|
|
1437
|
+
inverse,
|
|
1438
|
+
(
|
|
1439
|
+
lambda line: self.__shiftscale_action_geometry(
|
|
1440
|
+
geometry_scale,
|
|
1441
|
+
geometry_scale_origin,
|
|
1442
|
+
geometry_offset,
|
|
1443
|
+
resolution_old,
|
|
1444
|
+
resolution_new,
|
|
1445
|
+
bounds,
|
|
1446
|
+
used_styles,
|
|
1447
|
+
line,
|
|
1448
|
+
)
|
|
1449
|
+
),
|
|
1450
|
+
)
|
|
1451
|
+
|
|
1452
|
+
# New styles
|
|
1453
|
+
if geometry_new_styles:
|
|
1454
|
+
scale = (geometry_scale[0] + geometry_scale[1]) / 2.0
|
|
1455
|
+
for style_name, style_list in used_styles.items():
|
|
1456
|
+
for style in style_list:
|
|
1457
|
+
# Modify bounds
|
|
1458
|
+
align = style.Alignment
|
|
1459
|
+
ml, mr, mv = self.__shiftscale_action_get_new_margins(
|
|
1460
|
+
bounds,
|
|
1461
|
+
geometry_scale,
|
|
1462
|
+
resolution_new,
|
|
1463
|
+
align,
|
|
1464
|
+
None,
|
|
1465
|
+
style.MarginL,
|
|
1466
|
+
style.MarginR,
|
|
1467
|
+
style.MarginV,
|
|
1468
|
+
)
|
|
1469
|
+
style.MarginL = ml
|
|
1470
|
+
style.MarginR = mr
|
|
1471
|
+
style.MarginV = mv
|
|
1472
|
+
|
|
1473
|
+
# Modify scale
|
|
1474
|
+
style.Fontsize *= scale
|
|
1475
|
+
style.Spacing *= scale
|
|
1476
|
+
style.Outline *= scale
|
|
1477
|
+
style.Shadow *= scale
|
|
1478
|
+
|
|
1479
|
+
# Done
|
|
1480
|
+
return self
|
|
1481
|
+
|
|
1482
|
+
def __shiftscale_action_time(
|
|
1483
|
+
self,
|
|
1484
|
+
inverse,
|
|
1485
|
+
split_naive,
|
|
1486
|
+
time_scale,
|
|
1487
|
+
time_scale_origin,
|
|
1488
|
+
time_offset,
|
|
1489
|
+
time_clip_start,
|
|
1490
|
+
time_clip_end,
|
|
1491
|
+
line,
|
|
1492
|
+
):
|
|
1493
|
+
# Modify
|
|
1494
|
+
line.Start = (line.Start - time_scale_origin) * time_scale + time_scale_origin + time_offset
|
|
1495
|
+
line.End = (line.End - time_scale_origin) * time_scale + time_scale_origin + time_offset
|
|
1496
|
+
|
|
1497
|
+
# Modify timed tags
|
|
1498
|
+
line.Text = self.parse_text(
|
|
1499
|
+
line.Text,
|
|
1500
|
+
modify_tag=(lambda tag: self.__shiftscale_action_time_modify_tag(time_scale, tag)),
|
|
1501
|
+
)
|
|
1502
|
+
|
|
1503
|
+
# Clip
|
|
1504
|
+
if not inverse:
|
|
1505
|
+
# Keep inside
|
|
1506
|
+
if time_clip_start is not None:
|
|
1507
|
+
if line.End <= time_clip_start:
|
|
1508
|
+
line = None
|
|
1509
|
+
else:
|
|
1510
|
+
line_splits = self.__split_line(line, time_clip_start, split_naive)
|
|
1511
|
+
if line_splits is not None:
|
|
1512
|
+
line = line_splits[1]
|
|
1513
|
+
if time_clip_end is not None:
|
|
1514
|
+
if line.Start >= time_clip_end:
|
|
1515
|
+
line = None
|
|
1516
|
+
else:
|
|
1517
|
+
line_splits = self.__split_line(line, time_clip_end, split_naive)
|
|
1518
|
+
if line_splits is not None:
|
|
1519
|
+
line = line_splits[0]
|
|
1520
|
+
|
|
1521
|
+
# Done
|
|
1522
|
+
return line
|
|
1523
|
+
|
|
1524
|
+
def __shiftscale_action_time_modify_tag(self, time_scale, tag):
|
|
1525
|
+
tag_name = tag[0]
|
|
1526
|
+
if tag_name in ["k", "K", "kf", "ko"]:
|
|
1527
|
+
tag = list(tag)
|
|
1528
|
+
tag[1] = str(int(Formatters.str_to_number(tag[1]) * time_scale))
|
|
1529
|
+
elif tag_name == "move":
|
|
1530
|
+
if len(tag) == 7:
|
|
1531
|
+
tag = list(tag)
|
|
1532
|
+
tag[5] = str(int(Formatters.str_to_number(tag[5]) * time_scale))
|
|
1533
|
+
tag[6] = str(int(Formatters.str_to_number(tag[6]) * time_scale))
|
|
1534
|
+
elif tag_name == "fade":
|
|
1535
|
+
tag = list(tag)
|
|
1536
|
+
tag[4] = str(int(Formatters.str_to_number(tag[4]) * time_scale))
|
|
1537
|
+
tag[5] = str(int(Formatters.str_to_number(tag[5]) * time_scale))
|
|
1538
|
+
tag[6] = str(int(Formatters.str_to_number(tag[6]) * time_scale))
|
|
1539
|
+
tag[7] = str(int(Formatters.str_to_number(tag[7]) * time_scale))
|
|
1540
|
+
elif tag_name == "t":
|
|
1541
|
+
if len(tag) >= 4:
|
|
1542
|
+
tag = list(tag)
|
|
1543
|
+
tag[1] = str(int(Formatters.str_to_number(tag[1]) * time_scale))
|
|
1544
|
+
tag[2] = str(int(Formatters.str_to_number(tag[2]) * time_scale))
|
|
1545
|
+
|
|
1546
|
+
return [tag]
|
|
1547
|
+
|
|
1548
|
+
def __shiftscale_action_geometry(
|
|
1549
|
+
self,
|
|
1550
|
+
geometry_scale,
|
|
1551
|
+
geometry_scale_origin,
|
|
1552
|
+
geometry_offset,
|
|
1553
|
+
resolution_old,
|
|
1554
|
+
resolution_new,
|
|
1555
|
+
bounds,
|
|
1556
|
+
used_styles,
|
|
1557
|
+
line,
|
|
1558
|
+
):
|
|
1559
|
+
# Modify geometry
|
|
1560
|
+
state = {
|
|
1561
|
+
"align": None,
|
|
1562
|
+
}
|
|
1563
|
+
line.Text = self.parse_text(
|
|
1564
|
+
line.Text,
|
|
1565
|
+
modify_tag=(
|
|
1566
|
+
lambda tag: self.__shiftscale_action_geometry_modify_tag(state, geometry_scale, geometry_scale_origin, geometry_offset, tag)
|
|
1567
|
+
),
|
|
1568
|
+
modify_geometry=(
|
|
1569
|
+
lambda geo: self.__shiftscale_action_geometry_modify_geometry(geometry_scale, geometry_scale_origin, geometry_offset, geo)
|
|
1570
|
+
),
|
|
1571
|
+
)
|
|
1572
|
+
|
|
1573
|
+
# Modify bounds
|
|
1574
|
+
ml, mr, mv = self.__shiftscale_action_get_new_margins(
|
|
1575
|
+
bounds,
|
|
1576
|
+
geometry_scale,
|
|
1577
|
+
resolution_new,
|
|
1578
|
+
state["align"],
|
|
1579
|
+
line.Style,
|
|
1580
|
+
line.MarginL,
|
|
1581
|
+
line.MarginR,
|
|
1582
|
+
line.MarginV,
|
|
1583
|
+
)
|
|
1584
|
+
line.MarginL = ml
|
|
1585
|
+
line.MarginR = mr
|
|
1586
|
+
line.MarginV = mv
|
|
1587
|
+
|
|
1588
|
+
# Update styles
|
|
1589
|
+
if line.Style.Name not in used_styles:
|
|
1590
|
+
used_styles[line.Style.Name] = [line.Style]
|
|
1591
|
+
elif line.Style not in used_styles[line.Style.Name]:
|
|
1592
|
+
used_styles[line.Style.Name].append(line.Style)
|
|
1593
|
+
|
|
1594
|
+
# Done
|
|
1595
|
+
return line
|
|
1596
|
+
|
|
1597
|
+
def __shiftscale_action_geometry_modify_tag(self, state, geometry_scale, geometry_scale_origin, geometry_offset, tag):
|
|
1598
|
+
tag_name = tag[0]
|
|
1599
|
+
if tag_name in ["bord", "shad", "be", "blur", "fs"]:
|
|
1600
|
+
scale = (geometry_scale[0] + geometry_scale[1]) / 2.0
|
|
1601
|
+
tag = [
|
|
1602
|
+
tag_name,
|
|
1603
|
+
Formatters.number_to_str(Formatters.str_to_number(tag[1]) * scale),
|
|
1604
|
+
]
|
|
1605
|
+
elif tag_name in ["xbord", "xshad", "fsp"]:
|
|
1606
|
+
tag = [
|
|
1607
|
+
tag_name,
|
|
1608
|
+
Formatters.number_to_str(Formatters.str_to_number(tag[1]) * geometry_scale[0]),
|
|
1609
|
+
]
|
|
1610
|
+
elif tag_name in ["ybord", "yshad"]:
|
|
1611
|
+
tag = [
|
|
1612
|
+
tag_name,
|
|
1613
|
+
Formatters.number_to_str(Formatters.str_to_number(tag[1]) * geometry_scale[1]),
|
|
1614
|
+
]
|
|
1615
|
+
elif tag_name in ["pos", "org"]:
|
|
1616
|
+
tag = [
|
|
1617
|
+
tag_name,
|
|
1618
|
+
Formatters.number_to_str(
|
|
1619
|
+
(Formatters.str_to_number(tag[1]) - geometry_scale_origin[0]) * geometry_scale[0]
|
|
1620
|
+
+ geometry_scale_origin[0]
|
|
1621
|
+
+ geometry_offset[0]
|
|
1622
|
+
),
|
|
1623
|
+
Formatters.number_to_str(
|
|
1624
|
+
(Formatters.str_to_number(tag[2]) - geometry_scale_origin[1]) * geometry_scale[1]
|
|
1625
|
+
+ geometry_scale_origin[1]
|
|
1626
|
+
+ geometry_offset[1]
|
|
1627
|
+
),
|
|
1628
|
+
]
|
|
1629
|
+
elif tag_name in ["clip", "iclip"]:
|
|
1630
|
+
if len(tag) == 5:
|
|
1631
|
+
# Rectangle
|
|
1632
|
+
tag = list(tag)
|
|
1633
|
+
for i in range(1, len(tag)):
|
|
1634
|
+
xy = (i + 1) % 2
|
|
1635
|
+
val = Formatters.str_to_number(tag[i])
|
|
1636
|
+
val = (val - geometry_scale_origin[xy]) * geometry_scale[xy] + geometry_scale_origin[xy] + geometry_offset[xy]
|
|
1637
|
+
tag[i] = Formatters.number_to_str(val)
|
|
1638
|
+
else:
|
|
1639
|
+
# Draw command
|
|
1640
|
+
tag = list(tag)
|
|
1641
|
+
tag[-1] = self.__shiftscale_action_geometry_modify_geometry(geometry_scale, geometry_scale_origin, geometry_offset, tag[-1])
|
|
1642
|
+
elif tag_name == "move":
|
|
1643
|
+
tag = list(tag)
|
|
1644
|
+
for i in range(1, len(tag)):
|
|
1645
|
+
xy = (i + 1) % 2
|
|
1646
|
+
val = Formatters.str_to_number(tag[i])
|
|
1647
|
+
val = (val - geometry_scale_origin[xy]) * geometry_scale[xy] + geometry_scale_origin[xy] + geometry_offset[xy]
|
|
1648
|
+
tag[i] = Formatters.number_to_str(val)
|
|
1649
|
+
elif tag_name == "pbo":
|
|
1650
|
+
tag = [
|
|
1651
|
+
tag_name,
|
|
1652
|
+
str(int(Formatters.str_to_number(tag[1]) * geometry_scale[1])),
|
|
1653
|
+
]
|
|
1654
|
+
elif tag_name == "t":
|
|
1655
|
+
# Parse more tags
|
|
1656
|
+
tag[-1] = self.parse_tags(
|
|
1657
|
+
tag[-1],
|
|
1658
|
+
modify_tag=(
|
|
1659
|
+
lambda tag2: self.__shiftscale_action_geometry_modify_tag(
|
|
1660
|
+
None,
|
|
1661
|
+
geometry_scale,
|
|
1662
|
+
geometry_scale_origin,
|
|
1663
|
+
geometry_offset,
|
|
1664
|
+
tag2,
|
|
1665
|
+
)
|
|
1666
|
+
),
|
|
1667
|
+
)
|
|
1668
|
+
elif tag_name in ["a", "an"]:
|
|
1669
|
+
if tag_name == "a":
|
|
1670
|
+
align = self.__legacy_align_to_regular(Formatters.str_to_number(tag[1]))
|
|
1671
|
+
else: # if (tag_name == "an"):
|
|
1672
|
+
align = Formatters.str_to_number(tag[1])
|
|
1673
|
+
|
|
1674
|
+
# State update
|
|
1675
|
+
if state is not None and state["align"] is None:
|
|
1676
|
+
state["align"] = align
|
|
1677
|
+
|
|
1678
|
+
# Note: Middle vertical alignment will not always be properly positioned
|
|
1679
|
+
|
|
1680
|
+
return [tag]
|
|
1681
|
+
|
|
1682
|
+
def __shiftscale_action_geometry_modify_geometry(self, geometry_scale, geometry_scale_origin, geometry_offset, geo):
|
|
1683
|
+
points = self.__re_draw_command_split.split(geo.strip())
|
|
1684
|
+
xy = 0
|
|
1685
|
+
for i in range(len(points)):
|
|
1686
|
+
coord = points[i]
|
|
1687
|
+
if len(coord) == 1:
|
|
1688
|
+
coord_ord = ord(coord)
|
|
1689
|
+
if coord_ord >= self.__re_draw_commands_ord_min and coord_ord <= self.__re_draw_commands_ord_max:
|
|
1690
|
+
# New command
|
|
1691
|
+
xy = 0
|
|
1692
|
+
continue
|
|
1693
|
+
|
|
1694
|
+
# Value
|
|
1695
|
+
val = Formatters.str_to_number(coord)
|
|
1696
|
+
val = (val - geometry_scale_origin[xy]) * geometry_scale[xy] + geometry_scale_origin[xy] + geometry_offset[xy]
|
|
1697
|
+
points[i] = str(int(val))
|
|
1698
|
+
|
|
1699
|
+
# Next
|
|
1700
|
+
xy = (xy + 1) % 2
|
|
1701
|
+
|
|
1702
|
+
return " ".join(points)
|
|
1703
|
+
|
|
1704
|
+
def __shiftscale_action_get_new_bounds(
|
|
1705
|
+
self,
|
|
1706
|
+
geometry_scale,
|
|
1707
|
+
geometry_scale_origin,
|
|
1708
|
+
geometry_offset,
|
|
1709
|
+
resolution_old,
|
|
1710
|
+
resolution_new,
|
|
1711
|
+
):
|
|
1712
|
+
# Modify bounds
|
|
1713
|
+
return (
|
|
1714
|
+
geometry_offset[0],
|
|
1715
|
+
geometry_offset[1],
|
|
1716
|
+
(resolution_new[0] - resolution_old[0]) * geometry_scale[0] + geometry_offset[0],
|
|
1717
|
+
(resolution_new[1] - resolution_old[1]) * geometry_scale[1] + geometry_offset[1],
|
|
1718
|
+
)
|
|
1719
|
+
|
|
1720
|
+
def __shiftscale_action_get_new_margins(
|
|
1721
|
+
self,
|
|
1722
|
+
bounds,
|
|
1723
|
+
geometry_scale,
|
|
1724
|
+
resolution_new,
|
|
1725
|
+
align,
|
|
1726
|
+
style,
|
|
1727
|
+
margin_left,
|
|
1728
|
+
margin_right,
|
|
1729
|
+
margin_vertical,
|
|
1730
|
+
):
|
|
1731
|
+
# Default alignments
|
|
1732
|
+
if style is not None:
|
|
1733
|
+
if align is None:
|
|
1734
|
+
align = style.Alignment
|
|
1735
|
+
elif not style.fake and align != style.Alignment:
|
|
1736
|
+
margin_vertical = style.MarginV
|
|
1737
|
+
|
|
1738
|
+
# Modify
|
|
1739
|
+
if margin_left != 0:
|
|
1740
|
+
margin_left = margin_left * geometry_scale[0] + bounds[0]
|
|
1741
|
+
if margin_right != 0:
|
|
1742
|
+
margin_right = resolution_new[0] - (bounds[2] - margin_right * geometry_scale[0])
|
|
1743
|
+
if margin_vertical != 0:
|
|
1744
|
+
align_xy = self.get_xy_alignment(align)
|
|
1745
|
+
if align_xy[1] < 0: # Top
|
|
1746
|
+
margin_vertical = margin_vertical * geometry_scale[1] + bounds[1]
|
|
1747
|
+
elif align_xy[1] > 0: # Bottom
|
|
1748
|
+
margin_vertical = resolution_new[1] - (bounds[3] - margin_vertical * geometry_scale[1])
|
|
1749
|
+
else: # if (align_xy[1] == 0): # Middle
|
|
1750
|
+
margin_vertical = margin_vertical * geometry_scale[1]
|
|
1751
|
+
|
|
1752
|
+
# Return
|
|
1753
|
+
return (margin_left, margin_right, margin_vertical)
|
|
1754
|
+
|
|
1755
|
+
def loop(self, **kwargs): # Duplicate a timecode (range) for a certain length
|
|
1756
|
+
# Parse kwargs
|
|
1757
|
+
time = self.__kwarg_default(kwargs, "time", None)
|
|
1758
|
+
# timecode to loop; shortcut for both start/end
|
|
1759
|
+
start = self.__kwarg_default(kwargs, "start", time)
|
|
1760
|
+
# time to start at, or None for not bounded
|
|
1761
|
+
end = self.__kwarg_default(kwargs, "end", time)
|
|
1762
|
+
# time to start at, or None for not bounded
|
|
1763
|
+
|
|
1764
|
+
filter_types = self.__kwarg_default(kwargs, "filter_types", None)
|
|
1765
|
+
# list of event types to include; can be anything supporting the "in" operator; None means no filtering
|
|
1766
|
+
|
|
1767
|
+
length = self.__kwarg_default(kwargs, "length", None)
|
|
1768
|
+
# duration to loop for; if None, this is ignored
|
|
1769
|
+
count = self.__kwarg_default(kwargs, "count", None)
|
|
1770
|
+
# number of times to loop the extracted section; if start==end, or if None, this is ignored
|
|
1771
|
+
|
|
1772
|
+
# Exceptions
|
|
1773
|
+
if start is None and end is None:
|
|
1774
|
+
raise ValueError("start, end, or time must be specified")
|
|
1775
|
+
|
|
1776
|
+
if start is None:
|
|
1777
|
+
start = self.__get_minimum_timecode()
|
|
1778
|
+
start = min(start, end)
|
|
1779
|
+
elif end is None:
|
|
1780
|
+
end = self.__get_maximum_timecode()
|
|
1781
|
+
end = max(end, start)
|
|
1782
|
+
|
|
1783
|
+
if start > end:
|
|
1784
|
+
raise ValueError("start cannot be greater than end")
|
|
1785
|
+
|
|
1786
|
+
if count is None and length is None:
|
|
1787
|
+
raise ValueError("count and length cannot both be None")
|
|
1788
|
+
|
|
1789
|
+
if count is not None and count <= 0:
|
|
1790
|
+
raise ValueError("count cannot be 0 or negative")
|
|
1791
|
+
|
|
1792
|
+
if length is not None and length <= 0:
|
|
1793
|
+
raise ValueError("length cannot be 0 or negative")
|
|
1794
|
+
|
|
1795
|
+
# Cut parts
|
|
1796
|
+
temp = self.__class__()
|
|
1797
|
+
if start == end:
|
|
1798
|
+
i = 0
|
|
1799
|
+
i_max = len(self.events)
|
|
1800
|
+
while i < i_max:
|
|
1801
|
+
line = self.events[i]
|
|
1802
|
+
if filter_types is None or line.type in filter_types:
|
|
1803
|
+
# Attempt to split
|
|
1804
|
+
line_parts = self.__split_line3(line, start)
|
|
1805
|
+
if line_parts is not None:
|
|
1806
|
+
l_before, l_middle, l_after = line_parts
|
|
1807
|
+
|
|
1808
|
+
# Add to temp
|
|
1809
|
+
l_middle.End = l_middle.Start + length
|
|
1810
|
+
# Stretch
|
|
1811
|
+
temp.add(l_middle)
|
|
1812
|
+
|
|
1813
|
+
# Replace old
|
|
1814
|
+
if l_before is not None:
|
|
1815
|
+
self.events[i] = l_before
|
|
1816
|
+
if l_after is not None:
|
|
1817
|
+
self.events.append(l_after)
|
|
1818
|
+
elif l_after is not None:
|
|
1819
|
+
self.events[i] = l_after
|
|
1820
|
+
else:
|
|
1821
|
+
self.events.pop(i)
|
|
1822
|
+
i_max -= 1
|
|
1823
|
+
continue
|
|
1824
|
+
# Same as doing i -= 1, since something was removed and not replaced
|
|
1825
|
+
|
|
1826
|
+
# Next
|
|
1827
|
+
i += 1
|
|
1828
|
+
|
|
1829
|
+
# Modify count
|
|
1830
|
+
count = 1
|
|
1831
|
+
# Loop it exactly once
|
|
1832
|
+
length_single = length
|
|
1833
|
+
else:
|
|
1834
|
+
# Cut out a range
|
|
1835
|
+
self.extract(
|
|
1836
|
+
start=start,
|
|
1837
|
+
end=end,
|
|
1838
|
+
split=True,
|
|
1839
|
+
split_naive=False,
|
|
1840
|
+
full_inclusion=False,
|
|
1841
|
+
remove=True,
|
|
1842
|
+
other=temp,
|
|
1843
|
+
filter_types=filter_types,
|
|
1844
|
+
)
|
|
1845
|
+
|
|
1846
|
+
# Modify count
|
|
1847
|
+
if length is None:
|
|
1848
|
+
# Update length
|
|
1849
|
+
length = (end - start) * count
|
|
1850
|
+
elif count is None:
|
|
1851
|
+
# Update count
|
|
1852
|
+
count = length / float(end - start)
|
|
1853
|
+
else:
|
|
1854
|
+
# Stretch
|
|
1855
|
+
scale = length / float((end - start) * count)
|
|
1856
|
+
temp.shiftscale(time_scale=scale, time_scale_origin=start)
|
|
1857
|
+
|
|
1858
|
+
length_single = length / float(count)
|
|
1859
|
+
|
|
1860
|
+
# Modify Start/End of all lines AFTER "end"
|
|
1861
|
+
length -= end - start
|
|
1862
|
+
# account for the self.extract call
|
|
1863
|
+
for line in self.events:
|
|
1864
|
+
if (filter_types is None or line.type in filter_types) and line.Start >= end:
|
|
1865
|
+
line.Start += length
|
|
1866
|
+
line.End += length
|
|
1867
|
+
|
|
1868
|
+
# Merge temp
|
|
1869
|
+
time_offset = 0.0
|
|
1870
|
+
while count >= 1:
|
|
1871
|
+
# Merge
|
|
1872
|
+
self.merge(other=temp, remove=False, filter_types=None, time_shift=time_offset)
|
|
1873
|
+
|
|
1874
|
+
# Shift for next
|
|
1875
|
+
time_offset += length_single
|
|
1876
|
+
count -= 1
|
|
1877
|
+
if count > 0:
|
|
1878
|
+
# Cut
|
|
1879
|
+
temp.extract(start=start, end=start + length_single * count, inverse=True)
|
|
1880
|
+
# Add
|
|
1881
|
+
self.merge(other=temp, remove=True, filter_types=None, time_shift=time_offset)
|
|
1882
|
+
|
|
1883
|
+
# Done
|
|
1884
|
+
return self
|
|
1885
|
+
|
|
1886
|
+
def extract(self, **kwargs): # Copy/remove lines, possibly into another object
|
|
1887
|
+
# Parse kwargs
|
|
1888
|
+
start = self.__kwarg_default(kwargs, "start", None)
|
|
1889
|
+
# time to start at, or None for not bounded
|
|
1890
|
+
end = self.__kwarg_default(kwargs, "end", None)
|
|
1891
|
+
# time to start at, or None for not bounded
|
|
1892
|
+
|
|
1893
|
+
full_inclusion = self.__kwarg_default(kwargs, "full_inclusion", False)
|
|
1894
|
+
# if True, line timecodes must be fully included within the specified range
|
|
1895
|
+
inverse = self.__kwarg_default(kwargs, "inverse", False)
|
|
1896
|
+
# if True, operation is performed on all lines not included in the timecode range
|
|
1897
|
+
split = self.__kwarg_default(kwargs, "split", False)
|
|
1898
|
+
# if True, splits lines if they are not fully in the timecode range
|
|
1899
|
+
split_naive = self.__kwarg_default(kwargs, "split_naive", False)
|
|
1900
|
+
# if True, line splitting will not modify any formatting tags
|
|
1901
|
+
|
|
1902
|
+
filter_types = self.__kwarg_default(kwargs, "filter_types", None)
|
|
1903
|
+
# list of event types to include; can be anything supporting the "in" operator; None means no filtering
|
|
1904
|
+
|
|
1905
|
+
filter_function = self.__kwarg_default(kwargs, "filter_function", None)
|
|
1906
|
+
# custom function to filter lines: takes 1 argument (line) and should return True if it's kept, or False to remove
|
|
1907
|
+
|
|
1908
|
+
remove = self.__kwarg_default(kwargs, "remove", True)
|
|
1909
|
+
# if True, lines are removed from self
|
|
1910
|
+
other = self.__kwarg_default(kwargs, "other", None)
|
|
1911
|
+
# it not None, removes lines the other specified ASS instance
|
|
1912
|
+
|
|
1913
|
+
# Exceptions
|
|
1914
|
+
if start is not None and end is not None and start > end:
|
|
1915
|
+
raise ValueError("start cannot be greater than end")
|
|
1916
|
+
|
|
1917
|
+
# Split
|
|
1918
|
+
if split:
|
|
1919
|
+
self.__range_cut(filter_types, start, end, split_naive)
|
|
1920
|
+
|
|
1921
|
+
# Modify lines
|
|
1922
|
+
self.__range_action(
|
|
1923
|
+
filter_types,
|
|
1924
|
+
start,
|
|
1925
|
+
end,
|
|
1926
|
+
full_inclusion,
|
|
1927
|
+
inverse,
|
|
1928
|
+
(lambda line: self.__extract_action(other, remove, filter_function, line)),
|
|
1929
|
+
)
|
|
1930
|
+
|
|
1931
|
+
# Done
|
|
1932
|
+
return self
|
|
1933
|
+
|
|
1934
|
+
def __extract_action(self, other, remove, filter_function, line):
|
|
1935
|
+
if filter_function is None or filter_function(line):
|
|
1936
|
+
if remove:
|
|
1937
|
+
if other is not None:
|
|
1938
|
+
other.add(line)
|
|
1939
|
+
return None
|
|
1940
|
+
# removed
|
|
1941
|
+
elif other is not None:
|
|
1942
|
+
other.add(line.copy())
|
|
1943
|
+
|
|
1944
|
+
return line
|
|
1945
|
+
|
|
1946
|
+
def merge(self, **kwargs): # Merge with another subtitle object
|
|
1947
|
+
# Parse kwargs
|
|
1948
|
+
remove = self.__kwarg_default(kwargs, "remove", False)
|
|
1949
|
+
# if True, lines are removed from other
|
|
1950
|
+
filter_types = self.__kwarg_default(kwargs, "filter_types", None)
|
|
1951
|
+
# list of event types to include; can be anything supporting the "in" operator; None means no filtering
|
|
1952
|
+
other = self.__kwarg_default(kwargs, "other", None)
|
|
1953
|
+
# adds lines to THIS object from OTHER
|
|
1954
|
+
time_offset = self.__kwarg_default(kwargs, "time_offset", 0.0)
|
|
1955
|
+
# amount to offset line timings from other by
|
|
1956
|
+
|
|
1957
|
+
if other is None:
|
|
1958
|
+
raise ValueError("other cannot be None")
|
|
1959
|
+
|
|
1960
|
+
# Add
|
|
1961
|
+
i = 0
|
|
1962
|
+
i_max = len(other.events)
|
|
1963
|
+
while i < i_max:
|
|
1964
|
+
line = other.events[i]
|
|
1965
|
+
if filter_types is None or line.type in filter_types:
|
|
1966
|
+
# Add to self
|
|
1967
|
+
if remove:
|
|
1968
|
+
other.events.pop(i)
|
|
1969
|
+
else:
|
|
1970
|
+
line = line.copy()
|
|
1971
|
+
line.Start += time_offset
|
|
1972
|
+
line.End += time_offset
|
|
1973
|
+
self.add(line)
|
|
1974
|
+
|
|
1975
|
+
# Remove
|
|
1976
|
+
if remove:
|
|
1977
|
+
i_max -= 1
|
|
1978
|
+
continue
|
|
1979
|
+
# Same as doing i -= 1, since something was removed
|
|
1980
|
+
|
|
1981
|
+
# Next
|
|
1982
|
+
i += 1
|
|
1983
|
+
|
|
1984
|
+
# Done
|
|
1985
|
+
return self
|
|
1986
|
+
|
|
1987
|
+
def remove_formatting(self, **kwargs): # Remove special formatting from lines
|
|
1988
|
+
# Parse kwargs
|
|
1989
|
+
start = self.__kwarg_default(kwargs, "start", None)
|
|
1990
|
+
# time to start at, or None for not bounded
|
|
1991
|
+
end = self.__kwarg_default(kwargs, "end", None)
|
|
1992
|
+
# time to start at, or None for not bounded
|
|
1993
|
+
|
|
1994
|
+
full_inclusion = self.__kwarg_default(kwargs, "full_inclusion", False)
|
|
1995
|
+
# if True, line timecodes must be fully included within the specified range
|
|
1996
|
+
inverse = self.__kwarg_default(kwargs, "inverse", False)
|
|
1997
|
+
# if True, operation is performed on all lines not included in the timecode range
|
|
1998
|
+
split = self.__kwarg_default(kwargs, "split", False)
|
|
1999
|
+
# if True, splits lines if they are not fully in the timecode range
|
|
2000
|
+
split_naive = self.__kwarg_default(kwargs, "split_naive", False)
|
|
2001
|
+
# if True, line splitting will not modify any formatting tags
|
|
2002
|
+
|
|
2003
|
+
filter_types = self.__kwarg_default(kwargs, "filter_types", None)
|
|
2004
|
+
# list of event types to include; can be anything supporting the "in" operator; None means no filtering
|
|
2005
|
+
|
|
2006
|
+
remove_tags = self.__kwarg_default(kwargs, "tags", True)
|
|
2007
|
+
# True to remove
|
|
2008
|
+
remove_comments = self.__kwarg_default(kwargs, "comments", True)
|
|
2009
|
+
# True to remove
|
|
2010
|
+
remove_geometry = self.__kwarg_default(kwargs, "geometry", True)
|
|
2011
|
+
# True to remove
|
|
2012
|
+
remove_special = self.__kwarg_default(kwargs, "special", False)
|
|
2013
|
+
# True to remove
|
|
2014
|
+
|
|
2015
|
+
# Exceptions
|
|
2016
|
+
if start is not None and end is not None and start > end:
|
|
2017
|
+
raise ValueError("start cannot be greater than end")
|
|
2018
|
+
|
|
2019
|
+
# More setup
|
|
2020
|
+
modify_text = None
|
|
2021
|
+
modify_tag_block = None
|
|
2022
|
+
modify_tag = None
|
|
2023
|
+
modify_comment = None
|
|
2024
|
+
modify_geometry = None
|
|
2025
|
+
|
|
2026
|
+
if remove_tags and remove_comments and remove_geometry:
|
|
2027
|
+
# Faster version
|
|
2028
|
+
modify_tag_block = lambda b: ""
|
|
2029
|
+
modify_geometry = lambda g: ""
|
|
2030
|
+
else:
|
|
2031
|
+
# Generic version
|
|
2032
|
+
modify_tag_block = lambda b: ("" if (len(b) == 2) else b)
|
|
2033
|
+
if remove_comments:
|
|
2034
|
+
modify_comment = lambda c: ""
|
|
2035
|
+
if remove_geometry:
|
|
2036
|
+
modify_geometry = lambda g: ""
|
|
2037
|
+
modify_tag = lambda t: []
|
|
2038
|
+
else:
|
|
2039
|
+
modify_tag = lambda t: ([t] if (t[0] == "p") else [])
|
|
2040
|
+
|
|
2041
|
+
if remove_special:
|
|
2042
|
+
modify_text = lambda t: self.replace_special(t)
|
|
2043
|
+
|
|
2044
|
+
# Split
|
|
2045
|
+
if split:
|
|
2046
|
+
self.__range_cut(filter_types, start, end, split_naive)
|
|
2047
|
+
|
|
2048
|
+
# Modify lines
|
|
2049
|
+
self.__range_action(
|
|
2050
|
+
filter_types,
|
|
2051
|
+
start,
|
|
2052
|
+
end,
|
|
2053
|
+
full_inclusion,
|
|
2054
|
+
inverse,
|
|
2055
|
+
(
|
|
2056
|
+
lambda line: self.__remove_formatting_action(
|
|
2057
|
+
modify_text,
|
|
2058
|
+
modify_tag_block,
|
|
2059
|
+
modify_tag,
|
|
2060
|
+
modify_comment,
|
|
2061
|
+
modify_geometry,
|
|
2062
|
+
line,
|
|
2063
|
+
)
|
|
2064
|
+
),
|
|
2065
|
+
)
|
|
2066
|
+
|
|
2067
|
+
# Done
|
|
2068
|
+
return self
|
|
2069
|
+
|
|
2070
|
+
def __remove_formatting_action(
|
|
2071
|
+
self,
|
|
2072
|
+
modify_text,
|
|
2073
|
+
modify_tag_block,
|
|
2074
|
+
modify_tag,
|
|
2075
|
+
modify_comment,
|
|
2076
|
+
modify_geometry,
|
|
2077
|
+
line,
|
|
2078
|
+
):
|
|
2079
|
+
line.Text = self.parse_text(
|
|
2080
|
+
line.Text,
|
|
2081
|
+
modify_text=modify_text,
|
|
2082
|
+
modify_tag_block=modify_tag_block,
|
|
2083
|
+
modify_tag=modify_tag,
|
|
2084
|
+
modify_comment=modify_comment,
|
|
2085
|
+
modify_geometry=modify_geometry,
|
|
2086
|
+
)
|
|
2087
|
+
|
|
2088
|
+
return line
|