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.

Files changed (287) hide show
  1. karaoke_gen/__init__.py +38 -0
  2. karaoke_gen/audio_fetcher.py +1614 -0
  3. karaoke_gen/audio_processor.py +790 -0
  4. karaoke_gen/config.py +83 -0
  5. karaoke_gen/file_handler.py +387 -0
  6. karaoke_gen/instrumental_review/__init__.py +45 -0
  7. karaoke_gen/instrumental_review/analyzer.py +408 -0
  8. karaoke_gen/instrumental_review/editor.py +322 -0
  9. karaoke_gen/instrumental_review/models.py +171 -0
  10. karaoke_gen/instrumental_review/server.py +475 -0
  11. karaoke_gen/instrumental_review/static/index.html +1529 -0
  12. karaoke_gen/instrumental_review/waveform.py +409 -0
  13. karaoke_gen/karaoke_finalise/__init__.py +1 -0
  14. karaoke_gen/karaoke_finalise/karaoke_finalise.py +1833 -0
  15. karaoke_gen/karaoke_gen.py +1026 -0
  16. karaoke_gen/lyrics_processor.py +474 -0
  17. karaoke_gen/metadata.py +160 -0
  18. karaoke_gen/pipeline/__init__.py +87 -0
  19. karaoke_gen/pipeline/base.py +215 -0
  20. karaoke_gen/pipeline/context.py +230 -0
  21. karaoke_gen/pipeline/executors/__init__.py +21 -0
  22. karaoke_gen/pipeline/executors/local.py +159 -0
  23. karaoke_gen/pipeline/executors/remote.py +257 -0
  24. karaoke_gen/pipeline/stages/__init__.py +27 -0
  25. karaoke_gen/pipeline/stages/finalize.py +202 -0
  26. karaoke_gen/pipeline/stages/render.py +165 -0
  27. karaoke_gen/pipeline/stages/screens.py +139 -0
  28. karaoke_gen/pipeline/stages/separation.py +191 -0
  29. karaoke_gen/pipeline/stages/transcription.py +191 -0
  30. karaoke_gen/resources/AvenirNext-Bold.ttf +0 -0
  31. karaoke_gen/resources/Montserrat-Bold.ttf +0 -0
  32. karaoke_gen/resources/Oswald-Bold.ttf +0 -0
  33. karaoke_gen/resources/Oswald-SemiBold.ttf +0 -0
  34. karaoke_gen/resources/Zurich_Cn_BT_Bold.ttf +0 -0
  35. karaoke_gen/style_loader.py +531 -0
  36. karaoke_gen/utils/__init__.py +18 -0
  37. karaoke_gen/utils/bulk_cli.py +492 -0
  38. karaoke_gen/utils/cli_args.py +432 -0
  39. karaoke_gen/utils/gen_cli.py +978 -0
  40. karaoke_gen/utils/remote_cli.py +3268 -0
  41. karaoke_gen/video_background_processor.py +351 -0
  42. karaoke_gen/video_generator.py +424 -0
  43. karaoke_gen-0.75.54.dist-info/METADATA +718 -0
  44. karaoke_gen-0.75.54.dist-info/RECORD +287 -0
  45. karaoke_gen-0.75.54.dist-info/WHEEL +4 -0
  46. karaoke_gen-0.75.54.dist-info/entry_points.txt +5 -0
  47. karaoke_gen-0.75.54.dist-info/licenses/LICENSE +21 -0
  48. lyrics_transcriber/__init__.py +10 -0
  49. lyrics_transcriber/cli/__init__.py +0 -0
  50. lyrics_transcriber/cli/cli_main.py +285 -0
  51. lyrics_transcriber/core/__init__.py +0 -0
  52. lyrics_transcriber/core/config.py +50 -0
  53. lyrics_transcriber/core/controller.py +594 -0
  54. lyrics_transcriber/correction/__init__.py +0 -0
  55. lyrics_transcriber/correction/agentic/__init__.py +9 -0
  56. lyrics_transcriber/correction/agentic/adapter.py +71 -0
  57. lyrics_transcriber/correction/agentic/agent.py +313 -0
  58. lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
  59. lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
  60. lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
  61. lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
  62. lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
  63. lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
  64. lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
  65. lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
  66. lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
  67. lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
  68. lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
  69. lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
  70. lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
  71. lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
  72. lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
  73. lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
  74. lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
  75. lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
  76. lyrics_transcriber/correction/agentic/models/enums.py +38 -0
  77. lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
  78. lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
  79. lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
  80. lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
  81. lyrics_transcriber/correction/agentic/models/utils.py +19 -0
  82. lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
  83. lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
  84. lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
  85. lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
  86. lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
  87. lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
  88. lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
  89. lyrics_transcriber/correction/agentic/providers/base.py +36 -0
  90. lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
  91. lyrics_transcriber/correction/agentic/providers/config.py +73 -0
  92. lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
  93. lyrics_transcriber/correction/agentic/providers/health.py +28 -0
  94. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
  95. lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
  96. lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
  97. lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
  98. lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
  99. lyrics_transcriber/correction/agentic/router.py +35 -0
  100. lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
  101. lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
  102. lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
  103. lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
  104. lyrics_transcriber/correction/anchor_sequence.py +919 -0
  105. lyrics_transcriber/correction/corrector.py +760 -0
  106. lyrics_transcriber/correction/feedback/__init__.py +2 -0
  107. lyrics_transcriber/correction/feedback/schemas.py +107 -0
  108. lyrics_transcriber/correction/feedback/store.py +236 -0
  109. lyrics_transcriber/correction/handlers/__init__.py +0 -0
  110. lyrics_transcriber/correction/handlers/base.py +52 -0
  111. lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
  112. lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
  113. lyrics_transcriber/correction/handlers/llm.py +293 -0
  114. lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
  115. lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
  116. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
  117. lyrics_transcriber/correction/handlers/repeat.py +88 -0
  118. lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
  119. lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
  120. lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
  121. lyrics_transcriber/correction/handlers/word_operations.py +187 -0
  122. lyrics_transcriber/correction/operations.py +352 -0
  123. lyrics_transcriber/correction/phrase_analyzer.py +435 -0
  124. lyrics_transcriber/correction/text_utils.py +30 -0
  125. lyrics_transcriber/frontend/.gitignore +23 -0
  126. lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
  127. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  128. lyrics_transcriber/frontend/README.md +50 -0
  129. lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
  130. lyrics_transcriber/frontend/__init__.py +25 -0
  131. lyrics_transcriber/frontend/eslint.config.js +28 -0
  132. lyrics_transcriber/frontend/index.html +18 -0
  133. lyrics_transcriber/frontend/package.json +42 -0
  134. lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
  135. lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
  136. lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
  137. lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
  138. lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
  139. lyrics_transcriber/frontend/public/favicon.ico +0 -0
  140. lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
  141. lyrics_transcriber/frontend/src/App.tsx +214 -0
  142. lyrics_transcriber/frontend/src/api.ts +254 -0
  143. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
  144. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
  145. lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
  146. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
  147. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
  148. lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
  149. lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
  150. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
  151. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
  152. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
  153. lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
  154. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
  155. lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
  156. lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
  157. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
  158. lyrics_transcriber/frontend/src/components/Header.tsx +413 -0
  159. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1387 -0
  160. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +185 -0
  161. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +704 -0
  162. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +80 -0
  163. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +905 -0
  164. lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
  165. lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +127 -0
  166. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
  167. lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
  168. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
  169. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
  170. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +336 -0
  171. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
  172. lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
  173. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
  174. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
  175. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
  176. lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
  177. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
  178. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
  179. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
  180. lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
  181. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
  182. lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
  183. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  184. lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
  185. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
  186. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  187. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
  188. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
  189. lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
  190. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  191. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
  192. lyrics_transcriber/frontend/src/main.tsx +17 -0
  193. lyrics_transcriber/frontend/src/theme.ts +177 -0
  194. lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
  195. lyrics_transcriber/frontend/src/types.js +2 -0
  196. lyrics_transcriber/frontend/src/types.ts +199 -0
  197. lyrics_transcriber/frontend/src/validation.ts +132 -0
  198. lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
  199. lyrics_transcriber/frontend/tsconfig.app.json +26 -0
  200. lyrics_transcriber/frontend/tsconfig.json +25 -0
  201. lyrics_transcriber/frontend/tsconfig.node.json +23 -0
  202. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
  203. lyrics_transcriber/frontend/update_version.js +11 -0
  204. lyrics_transcriber/frontend/vite.config.d.ts +2 -0
  205. lyrics_transcriber/frontend/vite.config.js +10 -0
  206. lyrics_transcriber/frontend/vite.config.ts +11 -0
  207. lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
  208. lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
  209. lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
  210. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js +43288 -0
  211. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +1 -0
  212. lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
  213. lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
  214. lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
  215. lyrics_transcriber/frontend/web_assets/index.html +18 -0
  216. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
  217. lyrics_transcriber/frontend/yarn.lock +3752 -0
  218. lyrics_transcriber/lyrics/__init__.py +0 -0
  219. lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
  220. lyrics_transcriber/lyrics/file_provider.py +95 -0
  221. lyrics_transcriber/lyrics/genius.py +384 -0
  222. lyrics_transcriber/lyrics/lrclib.py +231 -0
  223. lyrics_transcriber/lyrics/musixmatch.py +156 -0
  224. lyrics_transcriber/lyrics/spotify.py +290 -0
  225. lyrics_transcriber/lyrics/user_input_provider.py +44 -0
  226. lyrics_transcriber/output/__init__.py +0 -0
  227. lyrics_transcriber/output/ass/__init__.py +21 -0
  228. lyrics_transcriber/output/ass/ass.py +2088 -0
  229. lyrics_transcriber/output/ass/ass_specs.txt +732 -0
  230. lyrics_transcriber/output/ass/config.py +180 -0
  231. lyrics_transcriber/output/ass/constants.py +23 -0
  232. lyrics_transcriber/output/ass/event.py +94 -0
  233. lyrics_transcriber/output/ass/formatters.py +132 -0
  234. lyrics_transcriber/output/ass/lyrics_line.py +265 -0
  235. lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
  236. lyrics_transcriber/output/ass/section_detector.py +89 -0
  237. lyrics_transcriber/output/ass/section_screen.py +106 -0
  238. lyrics_transcriber/output/ass/style.py +187 -0
  239. lyrics_transcriber/output/cdg.py +619 -0
  240. lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
  241. lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
  242. lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
  243. lyrics_transcriber/output/cdgmaker/config.py +151 -0
  244. lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
  245. lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
  246. lyrics_transcriber/output/cdgmaker/pack.py +507 -0
  247. lyrics_transcriber/output/cdgmaker/render.py +346 -0
  248. lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
  249. lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
  250. lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
  251. lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
  252. lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
  253. lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
  254. lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
  255. lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
  256. lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
  257. lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
  258. lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
  259. lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
  260. lyrics_transcriber/output/cdgmaker/utils.py +132 -0
  261. lyrics_transcriber/output/countdown_processor.py +306 -0
  262. lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
  263. lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
  264. lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
  265. lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
  266. lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
  267. lyrics_transcriber/output/fonts/arial.ttf +0 -0
  268. lyrics_transcriber/output/fonts/georgia.ttf +0 -0
  269. lyrics_transcriber/output/fonts/verdana.ttf +0 -0
  270. lyrics_transcriber/output/generator.py +257 -0
  271. lyrics_transcriber/output/lrc_to_cdg.py +61 -0
  272. lyrics_transcriber/output/lyrics_file.py +102 -0
  273. lyrics_transcriber/output/plain_text.py +96 -0
  274. lyrics_transcriber/output/segment_resizer.py +431 -0
  275. lyrics_transcriber/output/subtitles.py +397 -0
  276. lyrics_transcriber/output/video.py +544 -0
  277. lyrics_transcriber/review/__init__.py +0 -0
  278. lyrics_transcriber/review/server.py +676 -0
  279. lyrics_transcriber/storage/__init__.py +0 -0
  280. lyrics_transcriber/storage/dropbox.py +225 -0
  281. lyrics_transcriber/transcribers/__init__.py +0 -0
  282. lyrics_transcriber/transcribers/audioshake.py +379 -0
  283. lyrics_transcriber/transcribers/base_transcriber.py +157 -0
  284. lyrics_transcriber/transcribers/whisper.py +330 -0
  285. lyrics_transcriber/types.py +650 -0
  286. lyrics_transcriber/utils/__init__.py +0 -0
  287. 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